Skip to content

Commit

Permalink
Merge ada15c8 into 7635973
Browse files Browse the repository at this point in the history
  • Loading branch information
joshribakoff committed Nov 26, 2019
2 parents 7635973 + ada15c8 commit 49b759f
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 34 deletions.
33 changes: 28 additions & 5 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
ExploreBundleResult,
} from './index';
import { formatOutput, saveOutputToFile } from './output';
import url from 'url';
import { getFileContent } from './helpers';

/**
* Analyze bundle(s)
Expand All @@ -34,12 +36,33 @@ export async function explore(
// Get bundles from file tokens
bundles.push(...getBundles(fileTokens));

let coverageData;
if (options.coverage) {
coverageData = JSON.parse(getFileContent(options.coverage)).map(node => ({
...node,
url: url.parse(node.url).path,
}));
}

const results = await Promise.all(
bundles.map(bundle =>
exploreBundle(bundle, options).catch<ExploreErrorResult>(error =>
onExploreError(bundle, error)
)
)
bundles.map(bundle => {
let coverageDataForBundle;
if (coverageData) {
coverageDataForBundle = coverageData.find(
node => node.url !== '/' && bundle.code.includes(node.url)
);
if (!coverageDataForBundle) {
const examples = coverageData.map(n => n.url).join(', ');
throw new Error(
`Could not find coverage data for ${bundle.code}, does the file name match? Examples that were found: ${examples}`
);
}
}

return exploreBundle(bundle, options, coverageDataForBundle).catch<ExploreErrorResult>(
error => onExploreError(bundle, error)
);
})
);

const exploreResult = getExploreResult(results, options);
Expand Down
7 changes: 7 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface Arguments {
noRoot?: boolean;
replace?: string[];
with?: string[];
coverage?: string;
}

function parseArguments(): Arguments {
Expand All @@ -40,6 +41,11 @@ function parseArguments(): Arguments {
.example('$0 script.js --json result.json', 'Explore and save result as JSON to the file')
.demandCommand(1, 'At least one js file must be specified')
.options({
coverage: {
type: 'string',
description:
'If the path to a valid a chrome code coverage JSON export is supplied, the tree map will be colorized according to which percentage of the modules code was executed',
},
json: {
type: 'string',
description:
Expand Down Expand Up @@ -158,6 +164,7 @@ function getExploreOptions(argv: Arguments): ExploreOptions {
format: isString(argv.json) ? 'json' : isString(argv.tsv) ? 'tsv' : 'html',
filename: argv.json || argv.tsv || argv.html,
},
coverage: argv.coverage,
replaceMap,
onlyMapped: argv.onlyMapped,
excludeSourceMapComment: argv.excludeSourceMap,
Expand Down
39 changes: 33 additions & 6 deletions src/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@ import {
getOccurrencesCount,
} from './helpers';
import { AppError } from './app-error';
import { File, Bundle, ExploreOptions, ExploreBundleResult, FileSizes, FileSizeMap } from './index';
import {
File,
Bundle,
ExploreOptions,
ExploreBundleResult,
FileSizes,
FileSizeMap,
CoverageData,
} from './index';
import { findCoveredBytes } from './find-ranges';

export const UNMAPPED_KEY = '[unmapped]';
export const SOURCE_MAP_COMMENT_KEY = '[sourceMappingURL]';
Expand All @@ -21,15 +30,17 @@ export const SOURCE_MAP_COMMENT_KEY = '[sourceMappingURL]';
*/
export async function exploreBundle(
bundle: Bundle,
options: ExploreOptions
options: ExploreOptions,
coverageData: CoverageData
): Promise<ExploreBundleResult> {
const { code, map } = bundle;

const sourceMapData = await loadSourceMap(code, map);

const sizes = computeFileSizes(sourceMapData, options);
const sizes = computeFileSizes(sourceMapData, options, coverageData);

const files = adjustSourcePaths(sizes.files, options);
const filesCoverage = adjustSourcePaths(sizes.filesCoverage, options);

const { totalBytes, unmappedBytes, eolBytes, sourceMapCommentBytes } = sizes;

Expand All @@ -51,6 +62,7 @@ export async function exploreBundle(
eolBytes,
sourceMapCommentBytes,
files,
filesCoverage,
};
}

Expand Down Expand Up @@ -125,6 +137,12 @@ interface ComputeFileSizesContext {
eol: string;
}

export interface ModuleRange {
module: string;
start: number;
end: number;
}

function checkInvalidMappingColumn({
generatedLine,
generatedColumn,
Expand All @@ -147,7 +165,8 @@ function checkInvalidMappingColumn({
/** Calculate the number of bytes contributed by each source file */
function computeFileSizes(
sourceMapData: SourceMapData,
{ excludeSourceMapComment }: ExploreOptions
{ excludeSourceMapComment }: ExploreOptions,
coverageData: CoverageData
): FileSizes {
const { consumer, codeFileContent: fileContent } = sourceMapData;

Expand All @@ -164,6 +183,8 @@ function computeFileSizes(

consumer.computeColumnSpans();

const moduleRanges: ModuleRange[] = [];

consumer.eachMapping(({ source, generatedLine, generatedColumn, lastGeneratedColumn }) => {
// Columns are 0-based, Lines are 1-based

Expand Down Expand Up @@ -192,16 +213,21 @@ function computeFileSizes(
line,
eol,
});

mappingLength = lastGeneratedColumn - generatedColumn + 1;
} else {
mappingLength = Buffer.byteLength(line) - generatedColumn;
}

files[source] = (files[source] || 0) + mappingLength;
moduleRanges.push({
module: source,
start: generatedColumn,
end: lastGeneratedColumn || generatedColumn + line.length - 1,
});
mappedBytes += mappingLength;
});

const filesCoverage = coverageData ? findCoveredBytes(coverageData.ranges, moduleRanges) : {};

const sourceMapCommentBytes = Buffer.byteLength(sourceMapComment);
const eolBytes = getOccurrencesCount(eol, fileContent) * Buffer.byteLength(eol);
const totalBytes = Buffer.byteLength(fileContent);
Expand All @@ -214,6 +240,7 @@ function computeFileSizes(
...(excludeSourceMapComment
? { totalBytes: totalBytes - sourceMapCommentBytes }
: { totalBytes }),
filesCoverage
};
}

Expand Down
48 changes: 48 additions & 0 deletions src/find-ranges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Finds overlaps in arrays of byte ranges, using ratcheting pointers
* instead of nested loops for O(n) runtime instead of O(n^2)
*/
export function findCoveredBytes(coveredRanges, moduleRanges): { [module: string]: number } {
const sortedCoverageRanges = coveredRanges.sort((a, b) => a.start - b.start);
const sortedModuleRanges = moduleRanges.sort((a, b) => a.start - b.start);

let i = 0;
let j = 0;
const sizes = {};

while (i < moduleRanges.length && j < coveredRanges.length) {
const moduleRange = sortedModuleRanges[i];
const coverageRange = sortedCoverageRanges[j];

if (moduleRange.start <= coverageRange.end && moduleRange.end >= coverageRange.start) {
// overlaps, calculate amount, move to next coverage range
const end = Math.min(coverageRange.end, moduleRange.end);
const start = Math.max(moduleRange.start, coverageRange.start);
if (sizes[moduleRange.module] === undefined) {
sizes[moduleRange.module] = 0;
}
sizes[moduleRange.module] += end - start + 1;

if (
sortedModuleRanges[i + 1] !== undefined &&
sortedModuleRanges[i + 1].start <= coverageRange.end &&
sortedModuleRanges[i + 1].end >= coverageRange.start
) {
// next module also overlaps current coverage range, advance to next module instead of advancing coverage
i++;
} else {
// check next coverage range, it may also overlap this module range
j++;
}
} else if (moduleRange.end < coverageRange.start) {
// module comes entirely before coverageRange, check next module range
i++;
}
if (coverageRange.end < moduleRange.start) {
// module range comes entirely after coverage range, check next coverage range
j++;
}
}

return sizes;
}
37 changes: 31 additions & 6 deletions src/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import path from 'path';
import escapeHtml from 'escape-html';

import { formatBytes, getCommonPathPrefix, getFileContent, formatPercent } from './helpers';
import { ExploreBundleResult, FileSizeMap } from './index';
import { ExploreBundleResult, FileSizeMap, FileCoverageMap } from './index';

/**
* Generate HTML file content for specified files
Expand All @@ -32,14 +32,13 @@ export function generateHtml(exploreResults: ExploreBundleResult[]): string {
(result, data, index) => {
result[index] = {
name: data.bundleName,
data: getWebTreeMapData(data.files),
data: getWebTreeMapData(data),
};

return result;
},
{}
);

const template = getFileContent(path.join(__dirname, 'tree-viz.ejs'));

return ejs.render(template, {
Expand All @@ -56,6 +55,7 @@ export function generateHtml(exploreResults: ExploreBundleResult[]): string {
function makeMergedBundle(exploreResults: ExploreBundleResult[]): ExploreBundleResult {
let totalBytes = 0;
const files: FileSizeMap = {};
const filesCoverage: FileSizeMap = {};

// Remove any common prefix to keep the visualization as simple as possible.
const commonPrefix = getCommonPathPrefix(exploreResults.map(r => r.bundleName));
Expand All @@ -67,6 +67,9 @@ function makeMergedBundle(exploreResults: ExploreBundleResult[]): ExploreBundleR
Object.entries(result.files).forEach(([fileName, size]) => {
files[`${prefix}/${fileName}`] = size;
});
Object.entries(result.filesCoverage).forEach(([fileName, size]) => {
filesCoverage[`${prefix}/${fileName}`] = size;
});
}

return {
Expand All @@ -76,25 +79,29 @@ function makeMergedBundle(exploreResults: ExploreBundleResult[]): ExploreBundleR
eolBytes: 0,
sourceMapCommentBytes: 0,
files,
filesCoverage,
};
}

interface WebTreeMapNode {
name: string;
data: {
$area: number;
coveredSize?: number;
};
children?: WebTreeMapNode[];
}

/**
* Covert file size map to webtreemap data
*/
function getWebTreeMapData(files: FileSizeMap): WebTreeMapNode {
function getWebTreeMapData(data: ExploreBundleResult): WebTreeMapNode {
const files: FileSizeMap = data.files;
const filesCoverage: FileCoverageMap = data.filesCoverage;
const treeData = newNode('/');

for (const source in files) {
addNode(source, files[source], treeData);
addNode(source, files[source], filesCoverage[source], treeData);
}

addSizeToTitle(treeData, treeData.data['$area']);
Expand All @@ -107,15 +114,27 @@ function newNode(name: string): WebTreeMapNode {
name: escapeHtml(name),
data: {
$area: 0,
coveredSize: undefined,
},
};
}

function addNode(source: string, size: number, treeData: WebTreeMapNode): void {
function addNode(
source: string,
size: number,
coveredSize: number | undefined,
treeData: WebTreeMapNode
): void {
const parts = source.split('/');
let node = treeData;

node.data['$area'] += size;
if (coveredSize !== undefined) {
if (node.data.coveredSize === undefined) {
node.data['coveredSize'] = 0;
}
node.data.coveredSize += coveredSize;
}

parts.forEach(part => {
if (!node.children) {
Expand All @@ -131,6 +150,12 @@ function addNode(source: string, size: number, treeData: WebTreeMapNode): void {

node = child;
node.data['$area'] += size;
if (undefined !== coveredSize) {
if (node.data.coveredSize === undefined) {
node.data['coveredSize'] = 0;
}
node.data.coveredSize += coveredSize;
}
});
}

Expand Down
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ export default explore;
// Export all interfaces from index.ts to avoid type exports in compiled js code. See https://github.com/babel/babel/issues/8361

export type FileSizeMap = Record<string, number>;
export type FileCoverageMap = Record<string, number>;

export interface FileSizes {
files: FileSizeMap;
filesCoverage: FileCoverageMap;
unmappedBytes: number;
eolBytes: number;
sourceMapCommentBytes: number;
Expand Down Expand Up @@ -41,6 +43,15 @@ export interface Bundle {
map?: File;
}

export interface CoverageRange {
start: number;
end: number;
}

export interface CoverageData {
ranges: CoverageRange[];
}

export interface ExploreOptions {
/** Exclude "unmapped" bytes from the output */
onlyMapped?: boolean;
Expand All @@ -56,6 +67,7 @@ export interface ExploreOptions {
noRoot?: boolean;
/** Replace "this" by "that" map */
replaceMap?: ReplaceMap;
coverage?: string;
}

export interface ExploreResult {
Expand Down
1 change: 0 additions & 1 deletion src/tree-viz.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ const treeDataMap = <%- JSON.stringify(treeDataMap) %>;
function selectBundle(bundleId) {
const bundle = treeDataMap[bundleId];
appendTreemap(map, bundle.data);
document.title = bundle.name + ' - Source Map Explorer';
}
Expand Down

0 comments on commit 49b759f

Please sign in to comment.