diff --git a/packages/core/test/bundling/core_all/BUILD.bazel b/packages/core/test/bundling/core_all/BUILD.bazel new file mode 100644 index 0000000000..13b0f07a64 --- /dev/null +++ b/packages/core/test/bundling/core_all/BUILD.bazel @@ -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", + ], +) diff --git a/packages/core/test/bundling/core_all/bundle.golden_size_map.json b/packages/core/test/bundling/core_all/bundle.golden_size_map.json new file mode 100644 index 0000000000..3e5a7a82b6 --- /dev/null +++ b/packages/core/test/bundling/core_all/bundle.golden_size_map.json @@ -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 + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/core/test/bundling/core_all/index.ts b/packages/core/test/bundling/core_all/index.ts new file mode 100644 index 0000000000..17b3a12552 --- /dev/null +++ b/packages/core/test/bundling/core_all/index.ts @@ -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); diff --git a/tools/size-tracking/BUILD.bazel b/tools/size-tracking/BUILD.bazel new file mode 100644 index 0000000000..7a040f4832 --- /dev/null +++ b/tools/size-tracking/BUILD.bazel @@ -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", + ], +) diff --git a/tools/size-tracking/file_size_compare.ts b/tools/size-tracking/file_size_compare.ts new file mode 100644 index 0000000000..6ff91be7f6 --- /dev/null +++ b/tools/size-tracking/file_size_compare.ts @@ -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( + actual, expected, filePath, threshold); + } else { + return compareActualSizeToExpected(actual, 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; +} diff --git a/tools/size-tracking/file_size_compare_spec.ts b/tools/size-tracking/file_size_compare_spec.ts new file mode 100644 index 0000000000..ac8045bb93 --- /dev/null +++ b/tools/size-tracking/file_size_compare_spec.ts @@ -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./); + }); +}); diff --git a/tools/size-tracking/file_size_data.ts b/tools/size-tracking/file_size_data.ts new file mode 100644 index 0000000000..840242c2a6 --- /dev/null +++ b/tools/size-tracking/file_size_data.ts @@ -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; +} diff --git a/tools/size-tracking/file_size_data_spec.ts b/tools/size-tracking/file_size_data_spec.ts new file mode 100644 index 0000000000..8f125707a6 --- /dev/null +++ b/tools/size-tracking/file_size_data_spec.ts @@ -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}, + }, + }); + }); +}); diff --git a/tools/size-tracking/index.bzl b/tools/size-tracking/index.bzl new file mode 100644 index 0000000000..c2263f86f8 --- /dev/null +++ b/tools/size-tracking/index.bzl @@ -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 + ) diff --git a/tools/size-tracking/index.ts b/tools/size-tracking/index.ts new file mode 100644 index 0000000000..07035ffaed --- /dev/null +++ b/tools/size-tracking/index.ts @@ -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 = 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`); +} diff --git a/tools/size-tracking/size_tracker.ts b/tools/size-tracking/size_tracker.ts new file mode 100644 index 0000000000..ca96e113a1 --- /dev/null +++ b/tools/size-tracking/size_tracker.ts @@ -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] = (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 = 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; + } +} diff --git a/tools/size-tracking/size_tracking_spec.ts b/tools/size-tracking/size_tracking_spec.ts new file mode 100644 index 0000000000..56ee8b81d7 --- /dev/null +++ b/tools/size-tracking/size_tracking_spec.ts @@ -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, + }); + }); +});