Skip to content

Commit

Permalink
feat(bazel): move js mapping size tracking tool to dev-infra
Browse files Browse the repository at this point in the history
Moves the JS mapping size tracking tool from framework into the
dev-infra repo. The following additonal changes have been made:

* Reworked the API to not require manifest paths, but to support
  validatable Bazel labels as rule parameters.
* Adjustments to make it shippable from `bazel/`
* Additional testing to verify the Starlark code
* Update to account for the latest source-map types. i.e. making
  size-tracking asynchronous.
  • Loading branch information
devversion committed Jun 29, 2022
1 parent 96d3b40 commit 4627840
Show file tree
Hide file tree
Showing 20 changed files with 947 additions and 1 deletion.
1 change: 1 addition & 0 deletions .ng-dev/commit-message.mts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const commitMessage: CommitMessageConfig = {
'http-server',
'integration',
'karma',
'map-size-tracking',
'protos',
'remote-execution',
'spec-bundling',
Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ github-actions/commit-message-based-labels/main.js
github-actions/slash-commands/main.js
github-actions/post-approval-changes/main.js
.github/local-actions/changelog/main.js

bazel/map-size-tracking/test/size-golden.json
1 change: 1 addition & 0 deletions bazel/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ filegroup(
"//bazel/http-server:files",
"//bazel/integration:files",
"//bazel/karma:files",
"//bazel/map-size-tracking:files",
"//bazel/remote-execution:files",
"//bazel/spec-bundling:files",
],
Expand Down
22 changes: 22 additions & 0 deletions bazel/map-size-tracking/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
load("@npm//@bazel/concatjs:index.bzl", "ts_library")

package(default_visibility = ["//visibility:public"])

ts_library(
name = "map-size-tracking",
srcs = glob(["**/*.ts"]),
# A tsconfig needs to be specified as otherwise `ts_library` will look for the config
# in `//:package.json` and this breaks when the BUILD file is copied to `@npm//`.
tsconfig = "//:tsconfig.json",
deps = [
"@npm//@bazel/runfiles",
"@npm//@types/node",
"@npm//source-map",
],
)

# Make source files available for distribution via pkg_npm
filegroup(
name = "files",
srcs = glob(["*"]),
)
134 changes: 134 additions & 0 deletions bazel/map-size-tracking/file_size_compare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* @license
* Copyright Google LLC 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.js';

export interface SizeDifference {
filePath?: string;
message: string;
}

export interface Threshold {
/**
* Maximum difference percentage. Exceeding this causes a reported size
* difference. Percentage difference is helpful for small files where
* the byte threshold is not exceeded but the change is relatively large
* for the small file and should be reported.
*/
maxPercentageDiff: number;
/**
* Maximum byte difference. Exceeding this causes a reported size difference.
* The max byte threshold works good for large files where change is relatively
* small but still needs to reported as it causes an overall size regression.
*/
maxByteDiff: number;
}

/** Compares two file size data objects and returns an array of size differences. */
export function compareFileSizeData(
actual: FileSizeData,
expected: FileSizeData,
threshold: Threshold,
) {
return [
...compareSizeEntry(actual.files, expected.files, '/', threshold),
...compareActualSizeToExpected(actual.unmapped, expected.unmapped, '<unmapped>', threshold),
];
}

/** Compares two file size entries and returns an array of size differences. */
function compareSizeEntry(
actual: DirectorySizeEntry | number,
expected: DirectorySizeEntry | number,
filePath: string,
threshold: Threshold,
) {
if (typeof actual !== 'number' && typeof expected !== 'number') {
return compareDirectorySizeEntry(actual, expected, filePath, threshold);
} else {
return compareActualSizeToExpected(<number>actual, <number>expected, filePath, threshold);
}
}

/**
* Compares two size numbers and returns a size difference if the difference
* percentage exceeds the specified maximum percentage or the byte size
* difference exceeds the maximum byte difference.
*/
function compareActualSizeToExpected(
actualSize: number,
expectedSize: number,
filePath: string,
threshold: Threshold,
): SizeDifference[] {
const diffPercentage = getDifferencePercentage(actualSize, expectedSize);
const byteDiff = Math.abs(expectedSize - actualSize);
const diffs: SizeDifference[] = [];
if (diffPercentage > threshold.maxPercentageDiff) {
diffs.push({
filePath: filePath,
message:
`Differs by ${diffPercentage.toFixed(2)}% from the expected size ` +
`(actual = ${actualSize}, expected = ${expectedSize})`,
});
}
if (byteDiff > threshold.maxByteDiff) {
diffs.push({
filePath: filePath,
message:
`Differs by ${byteDiff}B from the expected size ` +
`(actual = ${actualSize}, expected = ${expectedSize})`,
});
}
return diffs;
}

/**
* 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: Threshold,
): 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;
}
73 changes: 73 additions & 0 deletions bazel/map-size-tracking/file_size_data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* @license
* Copyright Google LLC 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;
}
64 changes: 64 additions & 0 deletions bazel/map-size-tracking/index.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright Google LLC 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//:index.bzl", "nodejs_binary", "nodejs_test")

nodejs_args = ["--nobazel_run_linker"]

def js_mapping_size_test(
name,
src,
source_map,
golden_file,
max_percentage_diff,
max_byte_diff,
**kwargs):
"""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
Args:
name: Name of the test target
src: Label pointing to the script to be analyzed.
source_map: Label pointing to the JavaScript map file.
golden_file: Label pointing to the golden JSON file.
max_percentage_diff: Limit percentage difference that would result in test failures.
max_byte_diff: Limit relative byte difference that would result in test failures.
**kwargs: Additional arguments being passed to the NodeJS test target.
"""

all_data = ["//bazel/map-size-tracking", src, source_map, golden_file]
entry_point = "//bazel/map-size-tracking:index.ts"

nodejs_test(
name = name,
data = all_data,
entry_point = entry_point,
templated_args = nodejs_args + [
"$(rootpath %s)" % src,
"$(rootpath %s)" % source_map,
"$(rootpath %s)" % golden_file,
"%d" % max_percentage_diff,
"%d" % max_byte_diff,
"false",
],
**kwargs
)

nodejs_binary(
name = "%s.accept" % name,
testonly = True,
data = all_data,
entry_point = entry_point,
templated_args = nodejs_args + [
"$(rootpath %s)" % src,
"$(rootpath %s)" % source_map,
"$(rootpath %s)" % golden_file,
"0",
"0",
"true",
],
**kwargs
)
Loading

0 comments on commit 4627840

Please sign in to comment.