build: add size-tracking bazel test (#30070)

Introduces a new Bazel test that allows us to inspect
what source-files contribute to a given bundled file
and how much bytes they contribute to the bundle size.

Additionally the size-tracking rule groups the size
data by directories. This allows us to compare size
changes in the scope of directories. e.g. a lot of
files in a directory could increase slightly in size, but
in the directory scope the size change could be significant
and needs to be reported by the test target.

Resolves FW-1278

PR Close #30070
This commit is contained in:
Paul Gschwendtner 2019-04-23 20:50:11 +02:00 committed by Andrew Kushnir
parent a44b510087
commit 2945f47977
12 changed files with 1089 additions and 0 deletions

View File

@ -0,0 +1,42 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ng_rollup_bundle", "ts_library")
load("//tools/size-tracking:index.bzl", "js_size_tracking_test")
ts_library(
name = "core_all",
srcs = ["index.ts"],
tags = ["ivy-only"],
deps = [
"//packages/core",
],
)
ng_rollup_bundle(
name = "bundle",
entry_point = "packages/core/test/bundling/core_all/index.js",
tags = [
"ivy-only",
],
deps = [
":core_all",
"//packages/core",
"@npm//rxjs",
],
)
js_size_tracking_test(
name = "size_test",
src = "angular/packages/core/test/bundling/core_all/bundle.min.js",
data = [
":bundle",
":bundle.golden_size_map.json",
],
diffThreshold = 3,
goldenFile = "angular/packages/core/test/bundling/core_all/bundle.golden_size_map.json",
sourceMap = "angular/packages/core/test/bundling/core_all/bundle.min.js.map",
tags = [
"ivy-only",
"manual",
],
)

View File

@ -0,0 +1,362 @@
{
"unmapped": 25,
"files": {
"size": 268455,
"@angular/": {
"size": 248616,
"core/": {
"size": 248616,
"src/": {
"size": 248535,
"application_init.ts": 626,
"application_module.ts": 634,
"application_ref.ts": 7371,
"application_tokens.ts": 307,
"change_detection/": {
"size": 14119,
"change_detection.ts": 46,
"change_detection_util.ts": 822,
"change_detector_ref.ts": 93,
"constants.ts": 411,
"differs/": {
"size": 12747,
"default_iterable_differ.ts": 7623,
"default_keyvalue_differ.ts": 3882,
"iterable_differs.ts": 655,
"keyvalue_differs.ts": 587
}
},
"compiler/": {
"size": 442,
"compiler_facade.ts": 442
},
"console.ts": 217,
"debug/": {
"size": 7621,
"debug_node.ts": 7621
},
"di/": {
"size": 20079,
"forward_ref.ts": 211,
"injectable.ts": 82,
"injection_token.ts": 322,
"injector.ts": 3872,
"injector_compatibility.ts": 1005,
"interface/": {
"size": 484,
"defs.ts": 339,
"injector.ts": 145
},
"jit/": {
"size": 1988,
"environment.ts": 162,
"injectable.ts": 803,
"util.ts": 1023
},
"metadata.ts": 157,
"r3_injector.ts": 4765,
"reflective_errors.ts": 1376,
"reflective_injector.ts": 3062,
"reflective_key.ts": 661,
"reflective_provider.ts": 2000,
"scope.ts": 90,
"util.ts": 4
},
"error_handler.ts": 444,
"errors.ts": 175,
"event_emitter.ts": 952,
"i18n/": {
"size": 178,
"tokens.ts": 178
},
"interface/": {
"size": 222,
"simple_change.ts": 170,
"type.ts": 52
},
"ivy_switch.ts": 936,
"linker/": {
"size": 4923,
"compiler.ts": 825,
"component_factory.ts": 91,
"component_factory_resolver.ts": 1003,
"element_ref.ts": 119,
"ng_module_factory.ts": 78,
"ng_module_factory_loader.ts": 449,
"query_list.ts": 1011,
"system_js_ng_module_factory_loader.ts": 957,
"template_ref.ts": 97,
"view_container_ref.ts": 97,
"view_ref.ts": 196
},
"metadata/": {
"size": 3522,
"di.ts": 547,
"directives.ts": 604,
"ng_module.ts": 95,
"resource_loading.ts": 839,
"schema.ts": 1306,
"view.ts": 131
},
"platform_core_providers.ts": 118,
"profile/": {
"size": 442,
"profile.ts": 170,
"wtf_impl.ts": 272
},
"reflection/": {
"size": 4878,
"reflection.ts": 15,
"reflection_capabilities.ts": 3678,
"reflector.ts": 1185
},
"render/": {
"size": 482,
"api.ts": 482
},
"render3/": {
"size": 103297,
"bindings.ts": 300,
"component.ts": 4000,
"component_ref.ts": 2512,
"context_discovery.ts": 2098,
"definition.ts": 2486,
"di.ts": 3651,
"di_setup.ts": 1584,
"empty.ts": 16,
"errors.ts": 89,
"features/": {
"size": 2677,
"inherit_definition_feature.ts": 1993,
"ng_onchanges_feature.ts": 571,
"providers_feature.ts": 113
},
"fields.ts": 140,
"hooks.ts": 1843,
"i18n.ts": 14527,
"instructions/": {
"size": 20030,
"alloc_host_vars.ts": 290,
"change_detection.ts": 91,
"container.ts": 758,
"di.ts": 129,
"element.ts": 1214,
"element_container.ts": 335,
"embedded_view.ts": 678,
"get_current_view.ts": 26,
"listener.ts": 1401,
"next_context.ts": 44,
"projection.ts": 348,
"property.ts": 193,
"property_interpolation.ts": 2584,
"select.ts": 51,
"shared.ts": 10205,
"storage.ts": 169,
"styling.ts": 1329,
"text.ts": 185
},
"interfaces/": {
"size": 619,
"container.ts": 24,
"context.ts": 19,
"i18n.ts": 48,
"injector.ts": 242,
"renderer.ts": 176,
"view.ts": 110
},
"jit/": {
"size": 9479,
"directive.ts": 3409,
"environment.ts": 2758,
"module.ts": 3047,
"pipe.ts": 265
},
"metadata.ts": 615,
"ng_module_ref.ts": 986,
"node_manipulation.ts": 4571,
"node_selector_matcher.ts": 1780,
"node_util.ts": 335,
"pipe.ts": 958,
"players.ts": 564,
"pure_function.ts": 1273,
"query.ts": 3303,
"state.ts": 1442,
"styling/": {
"size": 11242,
"class_and_style_bindings.ts": 9074,
"core_player_handler.ts": 274,
"host_instructions_queue.ts": 335,
"player_factory.ts": 118,
"shared.ts": 5,
"state.ts": 55,
"util.ts": 1381
},
"tokens.ts": 10,
"util/": {
"size": 4102,
"attrs_utils.ts": 423,
"discovery_utils.ts": 1489,
"global_utils.ts": 374,
"injector_utils.ts": 150,
"misc_utils.ts": 625,
"view_traversal_utils.ts": 221,
"view_utils.ts": 820
},
"view_engine_compatibility.ts": 3815,
"view_engine_compatibility_prebound.ts": 38,
"view_ref.ts": 2212
},
"sanitization/": {
"size": 9766,
"bypass.ts": 669,
"html_sanitizer.ts": 4721,
"inert_body.ts": 2066,
"sanitization.ts": 1057,
"security.ts": 206,
"style_sanitizer.ts": 574,
"url_sanitizer.ts": 473
},
"testability/": {
"size": 3796,
"testability.ts": 3796
},
"util/": {
"size": 4317,
"array_utils.ts": 210,
"assert.ts": 81,
"closure.ts": 37,
"comparison.ts": 90,
"decorators.ts": 1640,
"errors.ts": 164,
"global.ts": 271,
"is_dev_mode.ts": 358,
"lang.ts": 109,
"microtask.ts": 159,
"ng_i18n_closure_mode.ts": 118,
"ng_reflect.ts": 334,
"property.ts": 201,
"stringify.ts": 290,
"symbol.ts": 255
},
"version.ts": 179,
"view/": {
"size": 55747,
"element.ts": 3814,
"entrypoint.ts": 962,
"errors.ts": 642,
"ng_content.ts": 447,
"ng_module.ts": 2448,
"provider.ts": 5363,
"pure_expression.ts": 2279,
"query.ts": 2385,
"refs.ts": 9337,
"services.ts": 11639,
"text.ts": 1551,
"types.ts": 768,
"util.ts": 4728,
"view.ts": 8143,
"view_attach.ts": 1241
},
"zone/": {
"size": 2745,
"ng_zone.ts": 2745
}
},
"test/": {
"size": 81,
"bundling/": {
"size": 81,
"core_all/": {
"size": 81,
"index.ts": 81
}
}
}
}
},
"external/": {
"size": 19814,
"npm/": {
"size": 19814,
"node_modules/": {
"size": 19814,
"rxjs/": {
"size": 18753,
"_esm5/": {
"size": 18753,
"internal/": {
"size": 18753,
"InnerSubscriber.js": 415,
"Notification.js": 15,
"Observable.js": 1420,
"Observer.js": 137,
"OuterSubscriber.js": 298,
"Subject.js": 1910,
"SubjectSubscription.js": 346,
"Subscriber.js": 3254,
"Subscription.js": 1536,
"config.js": 136,
"observable/": {
"size": 3191,
"ConnectableObservable.js": 1435,
"from.js": 245,
"fromArray.js": 186,
"fromIterable.js": 395,
"fromObservable.js": 347,
"fromPromise.js": 287,
"merge.js": 296
},
"operators/": {
"size": 3322,
"map.js": 624,
"mergeAll.js": 69,
"mergeMap.js": 1445,
"multicast.js": 415,
"refCount.js": 683,
"share.js": 82,
"windowToggle.js": 4
},
"symbol/": {
"size": 256,
"iterator.js": 104,
"observable.js": 64,
"rxSubscriber.js": 88
},
"util/": {
"size": 2517,
"EmptyError.js": 6,
"ObjectUnsubscribedError.js": 168,
"UnsubscriptionError.js": 279,
"canReportError.js": 114,
"hostReportError.js": 47,
"identity.js": 24,
"isArray.js": 67,
"isArrayLike.js": 74,
"isFunction.js": 42,
"isInteropObservable.js": 49,
"isIterable.js": 49,
"isObject.js": 51,
"isPromise.js": 84,
"isScheduler.js": 54,
"noop.js": 15,
"pipe.js": 105,
"subscribeTo.js": 434,
"subscribeToArray.js": 114,
"subscribeToIterable.js": 213,
"subscribeToObservable.js": 192,
"subscribeToPromise.js": 146,
"subscribeToResult.js": 74,
"toSubscriber.js": 116
}
}
}
},
"tslib/": {
"size": 1061,
"tslib.es6.js": 1061
}
}
}
}
}
}

View File

@ -0,0 +1,13 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as core from '@angular/core';
// We need to something with the "core" import in order to ensure
// that all symbols from core are preserved in the bundle.
console.error(core);

View File

@ -0,0 +1,35 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "size-tracking",
srcs = glob(
["**/*.ts"],
exclude = ["**/*_spec.ts"],
),
tsconfig = "//tools:tsconfig.json",
deps = [
"@npm//@types/node",
"@npm//@types/source-map",
],
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(["**/*_spec.ts"]),
deps = [
":size-tracking",
"@npm//@types/source-map",
],
)
jasmine_node_test(
name = "test",
data = [],
deps = [
":test_lib",
"@npm//source-map",
],
)

View File

@ -0,0 +1,96 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {DirectorySizeEntry, FileSizeData, getChildEntryNames} from './file_size_data';
export interface SizeDifference {
filePath?: string;
message: string;
}
/** Compares two file size data objects and returns an array of size differences. */
export function compareFileSizeData(
actual: FileSizeData, expected: FileSizeData, threshold: number) {
const diffs: SizeDifference[] = compareSizeEntry(actual.files, expected.files, '/', threshold);
const unmappedBytesDiff = getDifferencePercentage(actual.unmapped, expected.unmapped);
if (unmappedBytesDiff > threshold) {
diffs.push({
message: `Unmapped bytes differ by ${unmappedBytesDiff.toFixed(2)}% from ` +
`the expected size (actual = ${actual.unmapped}, expected = ${expected.unmapped})`
});
}
return diffs;
}
/** Compares two file size entries and returns an array of size differences. */
function compareSizeEntry(
actual: DirectorySizeEntry | number, expected: DirectorySizeEntry | number, filePath: string,
threshold: number) {
if (typeof actual !== 'number' && typeof expected !== 'number') {
return compareDirectorySizeEntry(
<DirectorySizeEntry>actual, <DirectorySizeEntry>expected, filePath, threshold);
} else {
return compareActualSizeToExpected(<number>actual, <number>expected, filePath, threshold);
}
}
/**
* Compares two size numbers and returns a size difference when the percentage difference
* exceeds the specified threshold.
*/
function compareActualSizeToExpected(
actualSize: number, expectedSize: number, filePath: string,
threshold: number): SizeDifference[] {
const diffPercentage = getDifferencePercentage(actualSize, expectedSize);
if (diffPercentage > threshold) {
return [{
filePath: filePath,
message: `Differs by ${diffPercentage.toFixed(2)}% from the expected size ` +
`(actual = ${actualSize}, expected = ${expectedSize})`
}];
}
return [];
}
/**
* Compares two size directory size entries and returns an array of found size
* differences within that directory.
*/
function compareDirectorySizeEntry(
actual: DirectorySizeEntry, expected: DirectorySizeEntry, filePath: string,
threshold: number): SizeDifference[] {
const diffs: SizeDifference[] =
[...compareActualSizeToExpected(actual.size, expected.size, filePath, threshold)];
getChildEntryNames(expected).forEach(childName => {
if (actual[childName] === undefined) {
diffs.push(
{filePath: filePath + childName, message: 'Expected file/directory is not included.'});
return;
}
diffs.push(...compareSizeEntry(
actual[childName], expected[childName], filePath + childName, threshold));
});
getChildEntryNames(actual).forEach(childName => {
if (expected[childName] === undefined) {
diffs.push({
filePath: filePath + childName,
message: 'Unexpected file/directory included (not part of golden).'
});
}
});
return diffs;
}
/** Gets the difference of the two size values in percentage. */
function getDifferencePercentage(actualSize: number, expectedSize: number) {
return (Math.abs(actualSize - expectedSize) / ((expectedSize + actualSize) / 2)) * 100;
}

View File

@ -0,0 +1,92 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {compareFileSizeData} from './file_size_compare';
describe('file size compare', () => {
it('should report if size entry differ by more than the specified threshold', () => {
const diffs = compareFileSizeData(
{
unmapped: 0,
files: {
size: 50,
'a.ts': 50,
}
},
{
unmapped: 0,
files: {
size: 75,
'a.ts': 75,
}
},
0);
expect(diffs.length).toBe(2);
expect(diffs[0].filePath).toBe('/');
expect(diffs[0].message).toMatch(/40.00% from the expected size/);
expect(diffs[1].filePath).toBe('/a.ts');
expect(diffs[1].message).toMatch(/40.00% from the expected size/);
});
it('should not report if size percentage difference does not exceed threshold', () => {
const diffs = compareFileSizeData(
{
unmapped: 0,
files: {
size: 50,
'a.ts': 50,
}
},
{
unmapped: 0,
files: {
size: 75,
'a.ts': 75,
}
},
40);
expect(diffs.length).toBe(0);
});
it('should report if expected file size data misses a file size entry', () => {
const diffs = compareFileSizeData(
{
unmapped: 0,
files: {
size: 101,
'a.ts': 100,
'b.ts': 1,
}
},
{unmapped: 0, files: {size: 100, 'a.ts': 100}}, 1);
expect(diffs.length).toBe(1);
expect(diffs[0].filePath).toBe('/b.ts');
expect(diffs[0].message).toMatch(/Unexpected file.*not part of golden./);
});
it('should report if actual file size data misses an expected file size entry', () => {
const diffs = compareFileSizeData(
{
unmapped: 0,
files: {
size: 100,
'a.ts': 100,
}
},
{unmapped: 0, files: {size: 101, 'a.ts': 100, 'b.ts': 1}}, 1);
expect(diffs.length).toBe(1);
expect(diffs[0].filePath).toBe('/b.ts');
expect(diffs[0].message).toMatch(/Expected file.*not included./);
});
});

View File

@ -0,0 +1,75 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export interface DirectorySizeEntry {
size: number;
[filePath: string]: DirectorySizeEntry|number;
}
export interface FileSizeData {
unmapped: number;
files: DirectorySizeEntry;
}
/** Returns a new file size data sorted by keys in ascending alphabetical order. */
export function sortFileSizeData({unmapped, files}: FileSizeData): FileSizeData {
return {unmapped, files: _sortDirectorySizeEntryObject(files)};
}
/** Gets the name of all child size entries of the specified one. */
export function getChildEntryNames(entry: DirectorySizeEntry): string[] {
// The "size" property is reserved for the stored size value.
return Object.keys(entry).filter(key => key !== 'size');
}
/**
* Returns the first size-entry that has multiple children. This is also known as
* the omitting of the common path prefix.
* */
export function omitCommonPathPrefix(entry: DirectorySizeEntry): DirectorySizeEntry {
let current: DirectorySizeEntry = entry;
while (getChildEntryNames(current).length === 1) {
const newChild = current[getChildEntryNames(current)[0]];
// Only omit the current node if it is a size entry. In case the new
// child is a holding a number, then this is a file and we don'twant
// to incorrectly omit the leaf file entries.
if (typeof newChild === 'number') {
break;
}
current = newChild;
}
return current;
}
function _sortDirectorySizeEntryObject(oldObject: DirectorySizeEntry): DirectorySizeEntry {
return Object.keys(oldObject)
.sort(_sortSizeEntryKeys)
.reduce(
(result, key) => {
if (typeof oldObject[key] === 'number') {
result[key] = oldObject[key];
} else {
result[key] = _sortDirectorySizeEntryObject(oldObject[key] as DirectorySizeEntry);
}
return result;
},
{} as DirectorySizeEntry);
}
function _sortSizeEntryKeys(a: string, b: string) {
// The "size" property should always be the first item in the size entry.
// This makes it easier to inspect the size of an entry in the golden.
if (a === 'size') {
return -1;
} else if (a < b) {
return -1;
} else if (a > b) {
return 1;
}
return 0;
}

View File

@ -0,0 +1,64 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FileSizeData, omitCommonPathPrefix, sortFileSizeData} from './file_size_data';
describe('file size data', () => {
it('should be able to properly omit the common path prefix', () => {
const data: FileSizeData = {
unmapped: 0,
files: {
size: 3,
'parent/': {
size: 3,
'parent2/': {
size: 3,
'a/': {
size: 3,
'file.ts': 3,
},
'b/': {
size: 0,
}
}
}
}
};
expect(omitCommonPathPrefix(data.files)).toEqual({
size: 3,
'a/': {
size: 3,
'file.ts': 3,
},
'b/': {
size: 0,
}
});
});
it('should be able to properly sort file size data in alphabetical order', () => {
const data: FileSizeData = {
unmapped: 0,
files: {
size: 7,
'b/': {'c.ts': 3, 'a.ts': 3, size: 6},
'a/': {'nested/': {size: 1, 'a.ts': 1}, size: 1},
}
};
expect(sortFileSizeData(data)).toEqual({
unmapped: 0,
files: {
size: 7,
'a/': {size: 1, 'nested/': {size: 1, 'a.ts': 1}},
'b/': {size: 6, 'a.ts': 3, 'c.ts': 3},
},
});
});
});

View File

@ -0,0 +1,39 @@
# Copyright Google Inc. All Rights Reserved.
#
# Use of this source code is governed by an MIT-style license that can be
# found in the LICENSE file at https://angular.io/license
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary", "nodejs_test")
"""
Macro that can be used to track the size of a given input file by inspecting
the corresponding source map. A golden file is used to compare the current
file size data against previously approved file size data
"""
def js_size_tracking_test(name, src, sourceMap, goldenFile, diffThreshold, data = [], **kwargs):
all_data = data + [
"//tools/size-tracking",
"@npm//source-map",
"@npm//chalk",
]
entry_point = "angular/tools/size-tracking/index.js"
nodejs_test(
name = name,
data = all_data,
entry_point = entry_point,
configuration_env_vars = ["compile"],
templated_args = [src, sourceMap, goldenFile, "%d" % diffThreshold, "false"],
**kwargs
)
nodejs_binary(
name = "%s.accept" % name,
testonly = True,
data = all_data,
entry_point = entry_point,
configuration_env_vars = ["compile"],
templated_args = [src, sourceMap, goldenFile, "%d" % diffThreshold, "true"],
**kwargs
)

View File

@ -0,0 +1,56 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {readFileSync, writeFileSync} from 'fs';
import {SizeTracker} from './size_tracker';
import chalk from 'chalk';
import {compareFileSizeData} from './file_size_compare';
import {FileSizeData} from './file_size_data';
if (require.main === module) {
const [filePath, sourceMapPath, goldenPath, thresholdArg, writeGoldenArg] = process.argv.slice(2);
const status = main(
require.resolve(filePath), require.resolve(sourceMapPath), require.resolve(goldenPath),
writeGoldenArg === 'true', parseInt(thresholdArg));
process.exit(status ? 0 : 1);
}
export function main(
filePath: string, sourceMapPath: string, goldenSizeMapPath: string, writeGolden: boolean,
diffThreshold: number): boolean {
const {sizeResult} = new SizeTracker(filePath, sourceMapPath);
if (writeGolden) {
writeFileSync(goldenSizeMapPath, JSON.stringify(sizeResult, null, 2));
console.error(chalk.green(`Updated golden size data in ${goldenSizeMapPath}`));
return;
}
const expectedSizeData = <FileSizeData>JSON.parse(readFileSync(goldenSizeMapPath, 'utf8'));
const differences = compareFileSizeData(sizeResult, expectedSizeData, diffThreshold);
if (!differences.length) {
return true;
}
console.error(
`Computed file size data does not match golden size data. ` +
`The following differences were found:\n`);
differences.forEach(({filePath, message}) => {
const failurePrefix = filePath ? `"${filePath}": ` : '';
console.error(chalk.red(` ${failurePrefix}${message}`));
});
const compile = process.env['compile'];
const defineFlag = (compile !== 'legacy') ? `--define=compile=${compile} ` : '';
const bazelTargetName = process.env['TEST_TARGET'];
console.error(`\nThe golden file can be updated with the following command:`);
console.error(` yarn bazel run ${defineFlag}${bazelTargetName}.accept`);
}

View File

@ -0,0 +1,104 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {readFileSync} from 'fs';
import {SourceMapConsumer} from 'source-map';
import {DirectorySizeEntry, FileSizeData, omitCommonPathPrefix, sortFileSizeData} from './file_size_data';
export class SizeTracker {
private fileContent: string;
private consumer: SourceMapConsumer;
/**
* Retraced size result that can be used to inspect where bytes in the input file
* originated from and how much each file contributes to the input file.
*/
readonly sizeResult: FileSizeData;
constructor(private filePath: string, private sourceMapPath: string) {
this.fileContent = readFileSync(filePath, 'utf8');
this.consumer = new SourceMapConsumer(JSON.parse(readFileSync(sourceMapPath, 'utf8')));
this.sizeResult = this._computeSizeResult();
}
/**
* Computes the file size data by analyzing the input file through the specified
* source-map.
*/
private _computeSizeResult(): FileSizeData {
const lines = this.fileContent.split(/(\r?\n)/);
const result: FileSizeData = {
unmapped: 0,
files: {size: 0},
};
// Walk through the columns for each line in the input file and find the
// origin source-file of the given character. This allows us to inspect
// how the given input file is composed and how much each individual file
// contributes to the overall bundle file.
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
const lineText = lines[lineIdx];
for (let colIdx = 0; colIdx < lineText.length; colIdx++) {
// Note that the "originalPositionFor" line number is one-based.
let {source} = this.consumer.originalPositionFor({line: lineIdx + 1, column: colIdx});
// Increase the amount of total bytes.
result.files.size += 1;
if (!source) {
result.unmapped += 1;
continue;
}
const pathSegments = this._resolveMappedPath(source).split('/');
let currentEntry = result.files;
// Walk through each path segment and update the size entries with
// new size. This makes it possibly to create na hierarchical tree
// that matches the actual file system.
pathSegments.forEach((segmentName, index) => {
// The last segment always refers to a file and we therefore can
// store the size verbatim as property value.
if (index === pathSegments.length - 1) {
currentEntry[segmentName] = (<number>currentEntry[segmentName] || 0) + 1;
} else {
// Append a trailing slash to the segment so that it
// is clear that this size entry represents a folder.
segmentName = `${segmentName}/`;
const newEntry = <DirectorySizeEntry>currentEntry[segmentName] || {size: 0};
newEntry.size += 1;
currentEntry = currentEntry[segmentName] = newEntry;
}
});
}
}
// Omit size entries which are not needed and just bloat up the file
// size data. e.g. if all paths start with "../../", we want to omit
// this prefix to make the size data less confusing.
result.files = omitCommonPathPrefix(result.files);
return sortFileSizeData(result);
}
private _resolveMappedPath(filePath: string): string {
// We only want to store POSIX-like paths in order to avoid path
// separator failures when running the golden tests on Windows.
filePath = filePath.replace(/\\/g, '/');
// Workaround for https://github.com/angular/angular/issues/30060
if (process.env['BAZEL_TARGET'].includes('test/bundling/core_all:size_test')) {
return filePath.replace(/^(\.\.\/)+external/, 'external')
.replace(/^(\.\.\/)+packages\/core\//, '@angular/core/')
.replace(/^(\.\.\/){3}/, '@angular/core/');
}
return filePath;
}
}

View File

@ -0,0 +1,111 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {writeFileSync} from 'fs';
import {join} from 'path';
import {SourceMapGenerator} from 'source-map';
import {SizeTracker} from './size_tracker';
const testTempDir = process.env['TEST_TMPDIR'] !;
describe('size tracking', () => {
let generator: SourceMapGenerator;
beforeEach(() => { generator = new SourceMapGenerator(); });
function writeFile(filePath: string, content: string): string {
const tmpFilePath = join(testTempDir, filePath);
writeFileSync(tmpFilePath, content);
return tmpFilePath;
}
it('should keep track of unmapped bytes in the file', () => {
generator.addMapping({
generated: {line: 1, column: 1},
original: {line: 1, column: 1},
source: './origin-a.ts',
});
// A => origin-a (2 bytes), U => unmapped (1 byte)
const mapPath = writeFile('/test.map', generator.toString());
const inputPath = writeFile('/test.js', `UAA`);
const {sizeResult} = new SizeTracker(inputPath, mapPath);
expect(sizeResult.unmapped).toBe(1);
expect(sizeResult.files).toEqual({
size: 3,
'origin-a.ts': 2,
});
});
it('should properly combine mapped characters from same source', () => {
generator.addMapping(
{generated: {line: 1, column: 0}, original: {line: 1, column: 0}, source: './origin-a.ts'});
generator.addMapping(
{generated: {line: 1, column: 1}, original: {line: 1, column: 0}, source: './origin-b.ts'});
generator.addMapping({
generated: {line: 1, column: 2},
original: {line: 10, column: 0},
source: './origin-a.ts'
});
// A => origin-a (1 byte), B => origin-b (two bytes)
const mapPath = writeFile('/test.map', generator.toString());
const inputPath = writeFile('/test.js', `ABB`);
const {sizeResult} = new SizeTracker(inputPath, mapPath);
expect(sizeResult.unmapped).toBe(0);
expect(sizeResult.files).toEqual({
size: 3,
'origin-a.ts': 2,
'origin-b.ts': 1,
});
});
it('should keep track of summed-up byte sizes for directories', () => {
generator.addMapping({
generated: {line: 1, column: 0},
original: {line: 1, column: 0},
source: '@angular/core/render3/a.ts'
});
generator.addMapping({
generated: {line: 1, column: 2},
original: {line: 1, column: 0},
source: '@angular/core/render3/b.ts'
});
generator.addMapping({
generated: {line: 1, column: 3},
original: {line: 1, column: 0},
source: '@angular/core/c.ts'
});
// A => render3/a.ts (2 bytes), B => render3/b.ts (1 byte), C => c.ts (1 byte)
const mapPath = writeFile('/test.map', generator.toString());
const inputPath = writeFile('/test.js', `AABC`);
const {sizeResult} = new SizeTracker(inputPath, mapPath);
expect(sizeResult.unmapped).toBe(0);
expect(sizeResult.files).toEqual({
size: 4,
'render3/': {
size: 3,
'a.ts': 2,
'b.ts': 1,
},
'c.ts': 1,
});
});
});