Skip to content

Commit

Permalink
fix: improve source map resolution efficiency
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus committed Jun 27, 2021
2 parents fd5ee1f + 8b2a25b commit a83b1e0
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 15 deletions.
13 changes: 11 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
language: node_js
node_js:
- node
- 8
# Fix Node 16 version to 16.2.0
# 16.3.0 is causing an issue with mock-fs library
# causing some unit tests in cauldron API to fail
# This is a temporary solution while waiting for
# this issue to be addressed by Node team in a new
# release and/or mock-fs library
# Reference : https://github.com/tschaub/mock-fs/issues/332
- 16.2.0
- 12
script:
- npm run lint
- npm run test
- npm run build
- npm run benchmark
- NODE_ENV=test nyc --silent ava
- nyc report --reporter=text-lcov | coveralls
- nyc check-coverage --lines 60
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"flow-bin": "^0.112.0",
"flow-copy-source": "^2.0.9",
"husky": "^3.1.0",
"mock-fs": "^5.0.0",
"nodemark": "^0.3.0",
"nyc": "^14.1.1"
},
"engines": {
Expand Down Expand Up @@ -67,7 +69,8 @@
"build": "rm -fr ./dist && NODE_ENV=production babel ./src --out-dir ./dist --copy-files --source-maps && flow-copy-source src dist",
"dev": "NODE_ENV=production babel ./src --out-dir ./dist --copy-files --source-maps --watch",
"lint": "eslint ./src ./test && flow",
"test": "NODE_ENV=test nyc ava --verbose --serial --concurrency 1"
"test": "NODE_ENV=test nyc ava --verbose --serial --concurrency 1",
"benchmark": "node performance/benchmark.js"
},
"version": "2.0.3"
}
14 changes: 14 additions & 0 deletions performance/benchmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable promise/prefer-await-to-callbacks */
/* eslint-disable no-console */
/* eslint-disable import/unambiguous */
/* eslint-disable import/no-commonjs */
const benchmark = require('nodemark');
const {getStackTrace} = require('../dist');

(async () => {
const result = await benchmark(async (callback) => {
await getStackTrace();
callback();
});
console.log(result);
})();
15 changes: 15 additions & 0 deletions performance/benchmark.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 19 additions & 2 deletions src/isReadableFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,29 @@

import fs from 'fs';

const fileAccessCache: {
[string]: boolean | typeof undefined,
...
} = {};

export default (filePath: string): boolean => {
// If the file was previously unreadable, we can assume this
// will always be the case
let accessable = fileAccessCache[filePath];

if (accessable !== undefined) {
return accessable;
}

try {
fs.accessSync(filePath, fs.constants.R_OK);

return true;
accessable = true;
} catch (error) {
return false;
accessable = false;
}

fileAccessCache[filePath] = accessable;

return accessable;
};
43 changes: 33 additions & 10 deletions src/resolveCallSiteSourceCodeLocation.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// @flow

import fs from 'fs';
import {promisify} from 'util';
import path from 'path';
import {
SourceMapConsumer,
NullableMappedPosition,
} from 'source-map';
import isCallSiteSourceCodeLocationResolvable from './isCallSiteSourceCodeLocationResolvable';
import isReadableFile from './isReadableFile';
Expand All @@ -12,6 +14,36 @@ import type {
SourceCodeLocationType,
} from './types';

const readFile = promisify(fs.readFile);

const cachedOriginalLines: { [string]: Promise<NullableMappedPosition> | typeof undefined, ... } = {};

const resolveOriginalPosition = (mapFilePath: string, column: number, line: number): Promise<NullableMappedPosition> => {
const lineKey = `${mapFilePath}-${line}-${column}`;

// if possible, attempt to resolve the original lines from cache
let originalLineResult = cachedOriginalLines[lineKey];

if (!originalLineResult) {
// Otherwise, consume the source map (hopefully from cache), and resolve the
// original line numbers
originalLineResult = (async () => {
const sourceMapResult = JSON.parse(await readFile(mapFilePath, 'utf8'));

return SourceMapConsumer.with(await sourceMapResult, undefined, (source) => {
return source.originalPositionFor({
column,
line,
});
});
})();
}

cachedOriginalLines[lineKey] = originalLineResult;

return originalLineResult;
};

export default async (callSite: CallSiteType): Promise<SourceCodeLocationType> => {
if (!isCallSiteSourceCodeLocationResolvable(callSite)) {
throw new Error('Cannot resolve source code location.');
Expand All @@ -34,16 +66,7 @@ export default async (callSite: CallSiteType): Promise<SourceCodeLocationType> =
};

if (isReadableFile(maybeMapFilePath)) {
const rawSourceMap = JSON.parse(fs.readFileSync(maybeMapFilePath, 'utf8'));

const consumer = await new SourceMapConsumer(rawSourceMap);

const originalPosition = consumer.originalPositionFor({
column: columnNumber,
line: lineNumber,
});

await consumer.destroy();
const originalPosition = await resolveOriginalPosition(maybeMapFilePath, columnNumber, lineNumber);

if (originalPosition.source) {
reportedNormalisedCallSite = {
Expand Down
91 changes: 91 additions & 0 deletions test/resolveCallSiteSourceCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import fs from 'fs';
import path from 'path';
import {promisify} from 'util';
import mockfs from 'mock-fs';
import test from 'ava';
import resolveCallSiteSourceCodeLocation from '../src/resolveCallSiteSourceCodeLocation';

const deleteFile = promisify(fs.unlink);
const writeFile = promisify(fs.writeFile);

const testSourceMap = {
file: 'min.js',
mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA',
names: [
'bar',
'baz',
'n',
],
sourceRoot: 'http://example.com/www/js/',
sources: [
'one.js',
'two.js',
],
version: 3,
};

mockfs({
node_modules:
mockfs.load('node_modules'),
'testsource.map': JSON.stringify(testSourceMap),
});

test('caches original line results', async (t) => {
const callSite = {
getColumnNumber: () => {
return 28;
},
getFileName: () => {
return 'testsource';
},
getLineNumber: () => {
return 2;
},
};
const originalCallsite1 = await resolveCallSiteSourceCodeLocation(callSite);

t.deepEqual(originalCallsite1, {columnNumber: 10,
fileName: `${path.resolve(path.join(__dirname, '../'))}/http:/example.com/www/js/two.js`,
lineNumber: 2});

// // When we delete the map, we still expect the in memory map to be used
await deleteFile('testsource.map');

const originalCallsite2 = await resolveCallSiteSourceCodeLocation(callSite);
t.deepEqual(originalCallsite2, originalCallsite1);
});

test('only attempts to access maps once', async (t) => {
const callSite1 = await resolveCallSiteSourceCodeLocation({
getColumnNumber: () => {
return 28;
},
getFileName: () => {
return 'somesource';
},
getLineNumber: () => {
return 2;
},
});
t.deepEqual(callSite1, {columnNumber: 28,
fileName: 'somesource',
lineNumber: 2});

await writeFile('sourcesource', JSON.stringify(testSourceMap));

// We don't expect it to attempt to read this source map again
const callSite2 = await resolveCallSiteSourceCodeLocation({
getColumnNumber: () => {
return 28;
},
getFileName: () => {
return 'somesource';
},
getLineNumber: () => {
return 2;
},
});
t.deepEqual(callSite2, {columnNumber: 28,
fileName: 'somesource',
lineNumber: 2});
});

0 comments on commit a83b1e0

Please sign in to comment.