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:
parent
a44b510087
commit
2945f47977
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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;
|
||||
}
|
|
@ -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./);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
)
|
|
@ -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`);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue