Skip to content

Commit 578c374

Browse files
fix: device logs are parsed slowly
Currently it takes around half a second to parse each console message from the application. For callstacks, which contain a lot of lines, which we try to map to original file, it takes several seconds. Also, at the moment, when the application reports that some log is from bundle.js/vendor.js, we parse the local files, which may not be the same files uploaded on the device. This may happen in cases where we have applied a hot module, the local bundle.js is changed and its source map is changed, but we have not uploaded it on the device yet. The reason for the slowness is that we read the whole original file every time when we see lines from the logs that points to it. We also create a new SourceMapConsumer for it. This means, that if the stacktrace contains 10 lines pointing to vendor.js, we'll read it 10 times. To handle both issues, set the sourceMapConsumer (and read the original file location) at the point when we upload files on the device. This way we'll always have in memory the same file as the one currently running on device and the source map will be correct. Also, this way the files will be parsed only once.
1 parent 2a9d433 commit 578c374

13 files changed

+108
-40
lines changed

lib/common/definitions/mobile.d.ts

+20
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ declare module Mobile {
164164
* Describes methods for providing device logs to a specific consumer.
165165
*/
166166
interface IDeviceLogProvider extends NodeJS.EventEmitter {
167+
/**
168+
* Sets the path to source file from which the logs are produced,
169+
* i.e. the original file location of the file running on device.
170+
* @param {string} pathToSourceFile Path to the source file.
171+
* @returns {Promise<void>}
172+
*/
173+
setSourceFileLocation(pathToSourceFile: string): Promise<void>;
174+
167175
/**
168176
* Logs data in the specific way for the consumer.
169177
* @param {string} line String from the device logs.
@@ -258,6 +266,12 @@ declare module Mobile {
258266
* Replaces file paths in device log with their original location
259267
*/
260268
interface ILogSourceMapService {
269+
/**
270+
* Sets the sourceMapConsumer instance for specified file.
271+
* @param {string} filePath Full path to a local file containing both content and inline source map.
272+
* @return {Promise<void>}
273+
*/
274+
setSourceMapConsumerForFile(filePath: string): Promise<void>;
261275
replaceWithOriginalFileLocations(platform: string, messageData: string, loggingOptions: Mobile.IDeviceLogOptions): string
262276
}
263277

@@ -307,6 +321,12 @@ declare module Mobile {
307321
tryStartApplication(appData: IApplicationData): Promise<void>;
308322
getDebuggableApps(): Promise<Mobile.IDeviceApplicationInformation[]>;
309323
getDebuggableAppViews(appIdentifiers: string[]): Promise<IDictionary<Mobile.IDebugWebViewInfo[]>>;
324+
/**
325+
* Sets the files transferred on device.
326+
* @param {string[]} files Local paths to files transferred on device.
327+
* @returns {Promise<void>}
328+
*/
329+
setTransferredAppFiles(files: string[]): Promise<void>;
310330
}
311331

312332
/**

lib/common/mobile/android/android-application-manager.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ export class AndroidApplicationManager extends ApplicationManagerBase {
1414
private $logcatHelper: Mobile.ILogcatHelper,
1515
private $androidProcessService: Mobile.IAndroidProcessService,
1616
private $httpClient: Server.IHttpClient,
17-
private $deviceLogProvider: Mobile.IDeviceLogProvider,
17+
protected $deviceLogProvider: Mobile.IDeviceLogProvider,
1818
private $errors: IErrors,
1919
$logger: ILogger,
2020
$hooksService: IHooksService) {
21-
super($logger, $hooksService);
21+
super($logger, $hooksService, $deviceLogProvider);
2222
}
2323

2424
public async getInstalledApplications(): Promise<string[]> {

lib/common/mobile/application-manager-base.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ export abstract class ApplicationManagerBase extends EventEmitter implements Mob
77
private lastAvailableDebuggableAppViews: IDictionary<Mobile.IDebugWebViewInfo[]> = {};
88

99
constructor(protected $logger: ILogger,
10-
protected $hooksService: IHooksService) {
10+
protected $hooksService: IHooksService,
11+
protected $deviceLogProvider: Mobile.IDeviceLogProvider) {
1112
super();
1213
}
1314

15+
public async setTransferredAppFiles(files: string[]): Promise<void> {
16+
for (const file of files) {
17+
await this.$deviceLogProvider.setSourceFileLocation(file);
18+
}
19+
}
20+
1421
public async reinstallApplication(appIdentifier: string, packageFilePath: string): Promise<void> {
1522
const isApplicationInstalled = await this.isApplicationInstalled(appIdentifier);
1623

lib/common/mobile/device-log-emitter.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ export class DeviceLogEmitter extends DeviceLogProviderBase {
55
constructor(protected $logFilter: Mobile.ILogFilter,
66
$logger: ILogger,
77
private $loggingLevels: Mobile.ILoggingLevels,
8-
private $logSourceMapService: Mobile.ILogSourceMapService) {
9-
super($logFilter, $logger);
8+
protected $logSourceMapService: Mobile.ILogSourceMapService) {
9+
super($logFilter, $logger, $logSourceMapService);
1010
}
1111

1212
public logData(line: string, platform: string, deviceIdentifier: string): void {

lib/common/mobile/device-log-provider-base.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ export abstract class DeviceLogProviderBase extends EventEmitter implements Mobi
55
protected devicesLogOptions: IDictionary<Mobile.IDeviceLogOptions> = {};
66

77
constructor(protected $logFilter: Mobile.ILogFilter,
8-
protected $logger: ILogger) {
8+
protected $logger: ILogger,
9+
protected $logSourceMapService: Mobile.ILogSourceMapService) {
910
super();
1011
}
1112

13+
public async setSourceFileLocation(pathToOriginalFile: string): Promise<void> {
14+
try {
15+
await this.$logSourceMapService.setSourceMapConsumerForFile(pathToOriginalFile);
16+
} catch (err) {
17+
this.$logger.trace("Error while trying to set source map file", err);
18+
}
19+
}
20+
1221
public abstract logData(lineText: string, platform: string, deviceIdentifier: string): void;
1322

1423
public abstract setLogLevel(logLevel: string, deviceIdentifier?: string): void;

lib/common/mobile/device-log-provider.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { LoggerConfigData } from "../../constants";
55
export class DeviceLogProvider extends DeviceLogProviderBase {
66
constructor(protected $logFilter: Mobile.ILogFilter,
77
protected $logger: ILogger,
8-
private $logSourceMapService: Mobile.ILogSourceMapService) {
9-
super($logFilter, $logger);
8+
protected $logSourceMapService: Mobile.ILogSourceMapService) {
9+
super($logFilter, $logger, $logSourceMapService);
1010
}
1111

1212
public logData(lineText: string, platform: string, deviceIdentifier: string): void {

lib/common/mobile/ios/device/ios-application-manager.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export class IOSApplicationManager extends ApplicationManagerBase {
1313
private $iOSNotificationService: IiOSNotificationService,
1414
private $iosDeviceOperations: IIOSDeviceOperations,
1515
private $options: IOptions,
16-
private $deviceLogProvider: Mobile.IDeviceLogProvider) {
17-
super($logger, $hooksService);
16+
protected $deviceLogProvider: Mobile.IDeviceLogProvider) {
17+
super($logger, $hooksService, $deviceLogProvider);
1818
}
1919

2020
public async getInstalledApplications(): Promise<string[]> {

lib/common/mobile/ios/simulator/ios-simulator-application-manager.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ export class IOSSimulatorApplicationManager extends ApplicationManagerBase {
1515
private device: Mobile.IiOSDevice,
1616
private $options: IOptions,
1717
private $fs: IFileSystem,
18-
private $deviceLogProvider: Mobile.IDeviceLogProvider,
18+
protected $deviceLogProvider: Mobile.IDeviceLogProvider,
1919
$logger: ILogger,
2020
$hooksService: IHooksService) {
21-
super($logger, $hooksService);
21+
super($logger, $hooksService, $deviceLogProvider);
2222
}
2323

2424
public async getInstalledApplications(): Promise<string[]> {

lib/common/test/unit-tests/mobile/application-manager-base.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { Yok } from "../../../yok";
22
import { assert } from "chai";
3-
import { CommonLoggerStub, HooksServiceStub } from "../stubs";
3+
import { CommonLoggerStub, HooksServiceStub, DeviceLogProviderStub } from "../stubs";
44
import { ApplicationManagerBase } from "../../../mobile/application-manager-base";
55

66
let currentlyAvailableAppsForDebugging: Mobile.IDeviceApplicationInformation[];
77
let currentlyInstalledApps: string[];
88
let currentlyAvailableAppWebViewsForDebugging: IDictionary<Mobile.IDebugWebViewInfo[]>;
99

1010
class ApplicationManager extends ApplicationManagerBase {
11-
constructor($logger: ILogger, $hooksService: IHooksService) {
12-
super($logger, $hooksService);
11+
constructor($logger: ILogger, $hooksService: IHooksService, $deviceLogProvider: Mobile.IDeviceLogProvider) {
12+
super($logger, $hooksService, $deviceLogProvider);
1313
}
1414

1515
public async installApplication(packageFilePath: string): Promise<void> {
@@ -46,6 +46,7 @@ function createTestInjector(): IInjector {
4646
testInjector.register("logger", CommonLoggerStub);
4747
testInjector.register("hooksService", HooksServiceStub);
4848
testInjector.register("applicationManager", ApplicationManager);
49+
testInjector.register("deviceLogProvider", DeviceLogProviderStub);
4950
return testInjector;
5051
}
5152

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

+3
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ export class DeviceLogProviderStub extends EventEmitter implements Mobile.IDevic
164164
public currentDeviceProjectNames: IStringDictionary = {};
165165
public currentDeviceProjectDirs: IStringDictionary = {};
166166

167+
async setSourceFileLocation(pathToSourceFile: string): Promise<void> {
168+
}
169+
167170
logData(line: string, platform: string, deviceIdentifier: string): void {
168171
this.logger.info(line, platform, deviceIdentifier, { [LoggerConfigData.skipNewLine]: true });
169172
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,14 @@ export abstract class PlatformLiveSyncServiceBase {
128128
};
129129
}
130130

131-
protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceData: ILiveSyncDeviceDescriptor, options: ITransferFilesOptions): Promise<Mobile.ILocalToDevicePathData[]> {
131+
private async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceData: ILiveSyncDeviceDescriptor, options: ITransferFilesOptions): Promise<Mobile.ILocalToDevicePathData[]> {
132132
let transferredFiles: Mobile.ILocalToDevicePathData[] = [];
133133
const deviceLiveSyncService = this.getDeviceLiveSyncService(deviceAppData.device, projectData);
134134

135135
transferredFiles = await deviceLiveSyncService.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, liveSyncDeviceData, options);
136136

137+
await deviceAppData.device.applicationManager.setTransferredAppFiles(localToDevicePaths.map(l => l.getLocalPath()));
138+
137139
this.logFilesSyncInformation(transferredFiles, "Successfully transferred %s on device %s.", this.$logger.info, deviceAppData.device.deviceInfo.identifier);
138140

139141
return transferredFiles;

lib/services/log-source-map-service.ts

+41-23
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,34 @@ interface IFileLocation {
2222
export class LogSourceMapService implements Mobile.ILogSourceMapService {
2323
private static FILE_PREFIX = "file:///";
2424
private getProjectData: Function;
25+
private cache: IDictionary<sourcemap.SourceMapConsumer> = {};
26+
2527
constructor(
2628
private $fs: IFileSystem,
2729
private $projectDataService: IProjectDataService,
2830
private $injector: IInjector,
29-
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants) {
30-
this.getProjectData = _.memoize(this.$projectDataService.getProjectData.bind(this.$projectDataService));
31+
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
32+
private $logger: ILogger) {
33+
this.getProjectData = _.memoize(this.$projectDataService.getProjectData.bind(this.$projectDataService));
34+
}
35+
36+
public async setSourceMapConsumerForFile(filePath: string): Promise<void> {
37+
try {
38+
if (!this.$fs.getFsStats(filePath).isDirectory()) {
39+
const source = this.$fs.readText(filePath);
40+
const sourceMapRaw = sourceMapConverter.fromSource(source);
41+
let smc: sourcemap.SourceMapConsumer = null;
42+
if (sourceMapRaw && sourceMapRaw.sourcemap) {
43+
const sourceMap = sourceMapRaw.sourcemap;
44+
smc = new sourcemap.SourceMapConsumer(sourceMap);
45+
}
46+
47+
this.cache[filePath] = smc;
48+
}
49+
} catch (err) {
50+
this.$logger.trace(`Unable to set sourceMapConsumer for file ${filePath}. Error is: ${err}`);
51+
}
52+
3153
}
3254

3355
public replaceWithOriginalFileLocations(platform: string, messageData: string, loggingOptions: Mobile.IDeviceLogOptions): string {
@@ -46,7 +68,7 @@ export class LogSourceMapService implements Mobile.ILogSourceMapService {
4668
const originalLocation = this.getOriginalFileLocation(platform, parsedLine, projectData);
4769

4870
if (originalLocation && originalLocation.sourceFile) {
49-
const {sourceFile, line, column} = originalLocation;
71+
const { sourceFile, line, column } = originalLocation;
5072
outputData = `${outputData}${parsedLine.messagePrefix}${LogSourceMapService.FILE_PREFIX}${sourceFile}:${line}:${column}${parsedLine.messageSuffix}\n`;
5173
} else if (rawLine !== "") {
5274
outputData = `${outputData}${rawLine}\n`;
@@ -57,25 +79,21 @@ export class LogSourceMapService implements Mobile.ILogSourceMapService {
5779
}
5880

5981
private getOriginalFileLocation(platform: string, parsedLine: IParsedMessage, projectData: IProjectData): IFileLocation {
60-
const fileLoaction = path.join(this.getFilesLocation(platform, projectData), APP_FOLDER_NAME);
82+
const fileLocation = path.join(this.getFilesLocation(platform, projectData), APP_FOLDER_NAME);
6183

6284
if (parsedLine && parsedLine.filePath) {
63-
const sourceMapFile = path.join(fileLoaction, parsedLine.filePath);
64-
if (this.$fs.exists(sourceMapFile)) {
65-
const source = this.$fs.readText(sourceMapFile);
66-
const sourceMapRaw = sourceMapConverter.fromSource(source);
67-
if (sourceMapRaw && sourceMapRaw.sourcemap) {
68-
const sourceMap = sourceMapRaw.sourcemap;
69-
const smc = new sourcemap.SourceMapConsumer(sourceMap);
70-
const originalPosition = smc.originalPositionFor({ line: parsedLine.line, column: parsedLine.column });
71-
let sourceFile = originalPosition.source && originalPosition.source.replace("webpack:///", "");
72-
if (sourceFile) {
73-
if (!_.startsWith(sourceFile, NODE_MODULES_FOLDER_NAME)) {
74-
sourceFile = path.join(projectData.getAppDirectoryRelativePath(), sourceFile);
75-
}
76-
sourceFile = stringReplaceAll(sourceFile, "/", path.sep);
77-
return { sourceFile, line: originalPosition.line, column: originalPosition.column};
85+
const sourceMapFile = path.join(fileLocation, parsedLine.filePath);
86+
const smc = this.cache[sourceMapFile];
87+
if (smc) {
88+
const originalPosition = smc.originalPositionFor({ line: parsedLine.line, column: parsedLine.column });
89+
let sourceFile = originalPosition.source && originalPosition.source.replace("webpack:///", "");
90+
if (sourceFile) {
91+
if (!_.startsWith(sourceFile, NODE_MODULES_FOLDER_NAME)) {
92+
sourceFile = path.join(projectData.getAppDirectoryRelativePath(), sourceFile);
7893
}
94+
95+
sourceFile = stringReplaceAll(sourceFile, "/", path.sep);
96+
return { sourceFile, line: originalPosition.line, column: originalPosition.column };
7997
}
8098
}
8199
}
@@ -111,7 +129,7 @@ export class LogSourceMapService implements Mobile.ILogSourceMapService {
111129
// "/data/data/org.nativescript.sourceMap/files/app/"
112130
const devicePath = `${deviceProjectPath}/${APP_FOLDER_NAME}/`;
113131
// "bundle.js"
114-
filePath = path.relative(devicePath, `${"/"}${parts[0]}`);
132+
filePath = path.relative(devicePath, `${"/"}${parts[0]}`);
115133
line = parseInt(parts[1]);
116134
column = parseInt(parts[2]);
117135
messagePrefix = rawMessage.substring(0, fileIndex);
@@ -123,7 +141,7 @@ export class LogSourceMapService implements Mobile.ILogSourceMapService {
123141
}
124142
}
125143

126-
return { filePath, line, column, messagePrefix, messageSuffix};
144+
return { filePath, line, column, messagePrefix, messageSuffix };
127145
}
128146

129147
private parseIosLog(rawMessage: string): IParsedMessage {
@@ -138,10 +156,10 @@ export class LogSourceMapService implements Mobile.ILogSourceMapService {
138156
parts = fileSubstring.split(":");
139157

140158
if (parts && parts.length >= 3) {
141-
filePath = parts[0];
159+
filePath = parts[0];
142160
// "app/vendor.js"
143161
if (_.startsWith(filePath, APP_FOLDER_NAME)) {
144-
filePath = path.relative(APP_FOLDER_NAME , parts[0]);
162+
filePath = path.relative(APP_FOLDER_NAME, parts[0]);
145163
}
146164
line = parseInt(parts[1]);
147165
column = parseInt(parts[2]);

test/services/log-source-map-service.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { LogSourceMapService } from "../../lib/services/log-source-map-service";
55
import { DevicePlatformsConstants } from "../../lib/common/mobile/device-platforms-constants";
66
import { FileSystem } from "../../lib/common/file-system";
77
import { stringReplaceAll } from "../../lib/common/helpers";
8+
import { LoggerStub } from "../stubs";
89

910
function createTestInjector(): IInjector {
1011
const testInjector = new Yok();
@@ -30,6 +31,7 @@ function createTestInjector(): IInjector {
3031
});
3132
testInjector.register("fs", FileSystem);
3233
testInjector.register("devicePlatformsConstants", DevicePlatformsConstants);
34+
testInjector.register("logger", LoggerStub);
3335
testInjector.register("logSourceMapService", LogSourceMapService);
3436

3537
return testInjector;
@@ -83,9 +85,15 @@ const testCases: IDictionary<Array<{caseName: string, message: string, expected:
8385
describe("log-source-map-service", () => {
8486
describe("replaceWithOriginalFileLocations", () => {
8587
let logSourceMapService: Mobile.ILogSourceMapService;
86-
before(() => {
88+
before(async () => {
8789
const testInjector = createTestInjector();
8890
logSourceMapService = testInjector.resolve("logSourceMapService");
91+
const originalFilesLocation = path.join(__dirname, ".." , "files", "sourceMapBundle");
92+
const fs = testInjector.resolve<IFileSystem>("fs");
93+
const files = fs.enumerateFilesInDirectorySync(originalFilesLocation);
94+
for (const file of files) {
95+
await logSourceMapService.setSourceMapConsumerForFile(file);
96+
}
8997
});
9098

9199
_.forEach(testCases, ( cases, platform ) => {

0 commit comments

Comments
 (0)