Bug 1350384 - Add ability to see a visualization of the test replicates in Perfherder (#2571)

This commit is contained in:
Shruti Jasoria 2017-07-21 21:45:59 +05:30 коммит произвёл William Lachance
Родитель 6db8fde645
Коммит dcac420965
11 изменённых файлов: 415 добавлений и 9 удалений

Просмотреть файл

@ -36,8 +36,7 @@ module.exports = neutrino => {
'angular', 'angular-cookies', 'angular-local-storage', 'angular-resource',
'angular-route', 'angular-sanitize', 'angular-toarrayfilter', 'angular-ui-bootstrap',
'angular-ui-router', 'bootstrap/dist/js/bootstrap', 'hawk', 'jquery', 'jquery.scrollto',
'js-yaml', 'mousetrap', 'react', 'react-dom', 'taskcluster-client', 'numeral'
];
'js-yaml', 'mousetrap', 'react', 'react-dom', 'taskcluster-client', 'numeral', 'metrics-graphics'];
jsDeps.map(dep =>
neutrino.config.entry('vendor').add(dep)
);

Просмотреть файл

@ -93,7 +93,8 @@ module.exports = neutrino => {
globals: ['angular', '$', '_', 'treeherder', 'perf',
'treeherderApp', 'failureViewerApp', 'logViewerApp',
'userguideApp', 'admin', 'Mousetrap', 'jQuery', 'React',
'hawk', 'jsonSchemaDefaults', 'SERVICE_DOMAIN', 'numeral'
'hawk', 'jsonSchemaDefaults', 'SERVICE_DOMAIN', 'numeral',
'metrics-graphics'
]
}
}));

Просмотреть файл

@ -20,6 +20,7 @@
"angular-ui-bootstrap": "1.3.3",
"angular-ui-router": "0.4.2",
"bootstrap": "3.3.7",
"d3": "^4.9.1",
"deepmerge": "1.3.2",
"font-awesome": "4.7.0",
"hawk": "6.0.1",
@ -29,6 +30,7 @@
"json-e": "^2.1.1",
"json-schema-defaults": "0.3.0",
"lodash": "4.17.4",
"metrics-graphics": "^2.11.0",
"mousetrap": "1.6.1",
"neutrino": "4.3.1",
"neutrino-lint-base": "4.3.1",

Просмотреть файл

@ -471,3 +471,17 @@ td.alert-confidence {
width: 200px;
border: 1px solid white;
}
.replicate-graph .mg-header {
font-family: "Helvetica";
font-size: 15px;
font-weight: bold;
}
.replicate-graph .mg-x-axis text {
font-size: 0px;
}
.replicate-graph .mg-category-guides line {
stroke: #ffffff;
}

Просмотреть файл

@ -10,6 +10,7 @@ require('font-awesome/css/font-awesome.css');
require('./css/treeherder-navbar.css');
require('./css/perf.css');
require('./css/treeherder-loading-overlay.css');
require('metrics-graphics/dist/metricsgraphics.css');
// Vendor JS
require('angular');
@ -22,6 +23,7 @@ require('mousetrap');
require('bootstrap/dist/js/bootstrap');
require('angular-ui-bootstrap');
require('numeral');
require('metrics-graphics');
require('./vendor/angular-clipboard.js');
// The jquery flot package does not seem to be updated on npm, so we use a local version:
require('./vendor/jquery.flot.js');

Просмотреть файл

@ -511,12 +511,13 @@ perf.controller('CompareSubtestResultsCtrl', [
'$state', '$stateParams', '$scope', '$rootScope', '$location',
'thServiceDomain', 'ThRepositoryModel',
'ThResultSetModel', '$http', '$q', '$timeout', 'PhSeries', 'math',
'PhCompare', 'phTimeRanges', 'compareBaseLineDefaultTimeRange',
'PhCompare', 'phTimeRanges', 'compareBaseLineDefaultTimeRange', '$httpParamSerializer',
function CompareSubtestResultsCtrl($state, $stateParams, $scope, $rootScope,
$location, thServiceDomain,
ThRepositoryModel, ThResultSetModel,
$http, $q, $timeout, PhSeries, math,
PhCompare, phTimeRanges, compareBaseLineDefaultTimeRange) {
PhCompare, phTimeRanges, compareBaseLineDefaultTimeRange,
$httpParamSerializer) {
//TODO: duplicated from comparectrl
function verifyRevision(project, revision, rsid) {
return ThResultSetModel.getResultSetsFromRevision(project.name, revision).then(
@ -588,6 +589,20 @@ perf.controller('CompareSubtestResultsCtrl', [
};
}), [$scope.originalResultSet, $scope.newResultSet])
}];
//replicate distribution is added only for talos
if ($scope.filterOptions.framework === '1') {
cmap.links.push({
title: 'replicate',
href: 'perf.html#/comparesubtestdistribution?' + $httpParamSerializer({
originalProject: $scope.originalProject.name,
newProject: $scope.newProject.name,
originalRevision: $scope.originalRevision,
newRevision: $scope.newRevision,
originalSubtestSignature: oldSig,
newSubtestSignature: newSig
})
});
}
}
else {
@ -605,7 +620,6 @@ perf.controller('CompareSubtestResultsCtrl', [
}), [$scope.newResultSet], $scope.selectedTimeRange.value)
}];
}
$scope.compareResults[testName].push(cmap);
});
}
@ -853,3 +867,113 @@ perf.controller('CompareSubtestResultsCtrl', [
});
});
}]);
perf.controller('CompareSubtestDistributionCtrl', ['$scope', '$stateParams', '$q', 'ThRepositoryModel',
'PhSeries', 'ThResultSetModel', 'metricsgraphics',
function CompareSubtestDistributionCtrl($scope, $stateParams, $q, ThRepositoryModel,
PhSeries, ThResultSetModel, metricsgraphics) {
$scope.originalRevision = $stateParams.originalRevision;
$scope.newRevision = $stateParams.newRevision;
$scope.originalSubtestSignature = $stateParams.originalSubtestSignature;
$scope.newSubtestSignature = $stateParams.newSubtestSignature;
$scope.dataLoading = true;
let loadRepositories = ThRepositoryModel.load();
const fetchAndDrawReplicateGraph = function (project, revision, subtestSignature, target) {
let replicateData = {};
return ThResultSetModel.getResultSetsFromRevision(project, revision).then(
(revisionData) => {
replicateData.resultSet = revisionData[0];
return PhSeries.getSeriesData(project, {
signatures: subtestSignature,
push_id: replicateData.resultSet.id
});
}).then((perfDatumList) => {
if (!perfDatumList[subtestSignature]) {
replicateData.replicateDataError = true;
return;
}
const numRuns = perfDatumList[subtestSignature].length;
let replicatePromises = perfDatumList[subtestSignature].map(
(value) => {return PhSeries.getReplicateData({job_id: value.job_id});
});
return $q.all(replicatePromises).then((replicateData) => {
let replicateValues = replicateData.concat.apply([],
replicateData.map((data) => {
let testSuite = data.suites.find((suite) => {
return suite.name === $scope.testSuite;
});
let subtest = testSuite.subtests.find((subtest) =>{
return subtest.name === $scope.subtest;
});
return subtest.replicates;
})
);
//metrics-graphics doesn't accept "0" as x_accesor
replicateValues = replicateValues.map((value, index) => ({
"replicate": (index + 1).toString(),
"value": value
}));
metricsgraphics.data_graphic({
title: `${target} Replicates over ${numRuns} run${(numRuns > 1) ? 's' : ''}`,
chart_type: "bar",
data: replicateValues,
y_accessor: "value",
x_accessor: "replicate",
height: 275,
width: 1000,
target: `#${target}`
});
},
() =>{
replicateData.replicateDataError = true;
});
}).then(() =>{
if (replicateData.replicateDataError) {
metricsgraphics.data_graphic({
title: `${target} Replicates`,
chart_type: 'missing-data',
missing_text: 'No Data Found',
target: `#${target}`,
width: 1000,
height: 275
});
}
return replicateData;
});
};
$q.all([loadRepositories]).then(() => {
$scope.originalProject = ThRepositoryModel.getRepo(
$stateParams.originalProject);
$scope.newProject = ThRepositoryModel.getRepo(
$stateParams.newProject);
PhSeries.getSeriesList($scope.originalProject.name, {signature: $scope.originalSubtestSignature}).then(
(seriesData) => {
$scope.testSuite = seriesData[0].suite;
$scope.subtest = seriesData[0].test;
$scope.testName = seriesData[0].name;
$scope.platform = seriesData[0].platform;
return fetchAndDrawReplicateGraph($scope.originalProject.name,
$scope.originalRevision,
$scope.originalSubtestSignature,
'Base');
}).then((result) => {
$scope.originalResultSet = result.resultSet;
$scope.originalReplicateError = result.replicateDataError;
if ($scope.originalReplicateError)
$scope.noResult = "base";
return fetchAndDrawReplicateGraph($scope.newProject.name,
$scope.newRevision,
$scope.newSubtestSignature,
'New');
}).then((result) => {
$scope.newResultSet = result.resultSet;
$scope.newReplicateError = result.replicateDataError;
if ($scope.newReplicateError)
$scope.noResult = "new";
window.document.title = `${$scope.platform}: ${$scope.testName}`;
$scope.dataLoading = false;
});
});
}
]);

Просмотреть файл

@ -81,12 +81,26 @@ treeherder.factory('PhSeries', ['$http', 'thServiceDomain', 'ThOptionCollectionM
},
getSeriesData: function (projectName, params) {
return $http.get(thServiceDomain + '/api/project/' + projectName + '/performance/data/',
{ params: params }).then(function (response) {
{params: params}).then(function (response) {
if (response.data) {
return response.data;
}
return $q.reject("No series data found");
});
},
getReplicateData: function (params) {
params.value = 'perfherder-data.json';
return $http.get(thServiceDomain + '/api/jobdetail/'
, {params: params}).then(
function (response) {
if (response.data.results[0]) {
let url = response.data.results[0].url;
return $http.get(url).then(function (response) {
return response.data;
});
}
return $q.reject("No replicate data found");
});
}
};
}]);

Просмотреть файл

@ -55,6 +55,11 @@ perf.config(['$compileProvider', '$httpProvider', '$stateProvider', '$urlRouterP
templateUrl: 'partials/perf/dashboardsubtest.html',
url: '/dashboardsubtest?topic&filter&showOnlyImportant&showOnlyConfident&baseSignature&variantSignature&repo&timerange&revision',
controller: 'dashSubtestCtrl'
}).state('comparesubtestdistribution', {
title: 'Compare Subtest Distribution',
templateUrl: 'partials/perf/comparesubtestdistribution.html',
url: '/comparesubtestdistribution?originalProject&newProject&originalRevision&newRevision&originalSubtestSignature?newSubtestSignature',
controller: 'CompareSubtestDistributionCtrl'
});
$urlRouterProvider.otherwise('/graphs');
}]).run(['$rootScope', '$state', '$stateParams', function ($rootScope, $state, $stateParams) {

Просмотреть файл

@ -218,3 +218,8 @@ treeherder.factory('numeral', [
function () {
return require('numeral');
}]);
treeherder.factory('metricsgraphics', [
function () {
return require('metrics-graphics');
}]);

Просмотреть файл

@ -0,0 +1,16 @@
<div class="container-fluid">
<div ng-show="dataLoading">
Loading all results, please wait a minute...
<img src="img/dancing_cat.gif" />
</div>
<div ng-show="!dataLoading">
<h2>{{platform}}: {{testName}} replicate distribution</h2>
<revision-information original-project="originalProject" original-revision="originalRevision" original-result-set="originalResultSet" new-project="newProject" new-revision="newRevision" new-result-set="newResultSet" selected-time-range="selectedTimeRange"></revision-information>
&nbsp;&nbsp;
<div ng-show="!(originalReplicateError && newReplicateError)">
<div id="Base" class="replicate-graph"></div>
<div id="New" class="replicate-graph"></div>
</div>
</div>
<p class="lead text-center" ng-show="originalReplicateError && newReplicateError">No results to show for these two revisions.</p>
</div>

228
yarn.lock
Просмотреть файл

@ -1501,7 +1501,7 @@ combined-stream@~0.0.4:
dependencies:
delayed-stream "0.0.5"
commander@2.9.0, commander@2.9.x:
commander@2, commander@2.9.0, commander@2.9.x:
version "2.9.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
dependencies:
@ -1804,6 +1804,216 @@ custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
d3-array@1, d3-array@1.2.0, d3-array@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.0.tgz#147d269720e174c4057a7f42be8b0f3f2ba53108"
d3-axis@1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.7.tgz#048433d307061f62d1d248e2930c01d7b6738cd8"
d3-brush@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4"
dependencies:
d3-dispatch "1"
d3-drag "1"
d3-interpolate "1"
d3-selection "1"
d3-transition "1"
d3-chord@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.4.tgz#7dec4f0ba886f713fe111c45f763414f6f74ca2c"
dependencies:
d3-array "1"
d3-path "1"
d3-collection@1, d3-collection@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.3.tgz#00bdea94fbc1628d435abbae2f4dc2164e37dd34"
d3-color@1, d3-color@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
d3-dispatch@1, d3-dispatch@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
d3-drag@1, d3-drag@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.1.0.tgz#4a49b4d77a42e9e3d5a0ef3b492b14aaa2e5a733"
dependencies:
d3-dispatch "1"
d3-selection "1"
d3-dsv@1, d3-dsv@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.0.5.tgz#419f7db47f628789fc3fdb636e678449d0821136"
dependencies:
commander "2"
iconv-lite "0.4"
rw "1"
d3-ease@1, d3-ease@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
d3-force@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.0.6.tgz#ea7e1b7730e2664cd314f594d6718c57cc132b79"
dependencies:
d3-collection "1"
d3-dispatch "1"
d3-quadtree "1"
d3-timer "1"
d3-format@1, d3-format@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.0.tgz#6b480baa886885d4651dc248a8f4ac9da16db07a"
d3-geo@1.6.4:
version "1.6.4"
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.6.4.tgz#f20e1e461cb1845f5a8be55ab6f876542a7e3199"
dependencies:
d3-array "1"
d3-hierarchy@1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.4.tgz#96c3942f3f21cf997a11b4edf00dde2a77b4c6d0"
d3-interpolate@1, d3-interpolate@1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.5.tgz#69e099ff39214716e563c9aec3ea9d1ea4b8a79f"
dependencies:
d3-color "1"
d3-path@1, d3-path@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764"
d3-polygon@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.3.tgz#16888e9026460933f2b179652ad378224d382c62"
d3-quadtree@1, d3-quadtree@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.3.tgz#ac7987e3e23fe805a990f28e1b50d38fcb822438"
d3-queue@3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-3.0.7.tgz#c93a2e54b417c0959129d7d73f6cf7d4292e7618"
d3-random@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.0.tgz#6642e506c6fa3a648595d2b2469788a8d12529d3"
d3-request@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/d3-request/-/d3-request-1.0.5.tgz#4daae946d1dd0d57dfe01f022956354958d51f23"
dependencies:
d3-collection "1"
d3-dispatch "1"
d3-dsv "1"
xmlhttprequest "1"
d3-scale@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.6.tgz#bce19da80d3a0cf422c9543ae3322086220b34ed"
dependencies:
d3-array "^1.2.0"
d3-collection "1"
d3-color "1"
d3-format "1"
d3-interpolate "1"
d3-time "1"
d3-time-format "2"
d3-selection@1, d3-selection@1.1.0, d3-selection@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.1.0.tgz#1998684896488f839ca0372123da34f1d318809c"
d3-shape@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.1.1.tgz#50a1037e48a79f5b8fd9d58cde52799aeb1f7723"
dependencies:
d3-path "1"
d3-time-format@2, d3-time-format@2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.0.5.tgz#9d7780204f7c9119c9170b1a56db4de9a8af972e"
dependencies:
d3-time "1"
d3-time@1, d3-time@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.6.tgz#a55b13d7d15d3a160ae91708232e0835f1d5e945"
d3-timer@1, d3-timer@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.5.tgz#b266d476c71b0d269e7ac5f352b410a3b6fe6ef0"
d3-transition@1, d3-transition@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.0.tgz#cfc85c74e5239324290546623572990560c3966f"
dependencies:
d3-color "1"
d3-dispatch "1"
d3-ease "1"
d3-interpolate "1"
d3-selection "^1.1.0"
d3-timer "1"
d3-voronoi@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c"
d3-zoom@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.2.0.tgz#b3231f4f9386241475defe1c557bfd3fde1065fb"
dependencies:
d3-dispatch "1"
d3-drag "1"
d3-interpolate "1"
d3-selection "1"
d3-transition "1"
d3@^4, d3@^4.9.1:
version "4.9.1"
resolved "https://registry.yarnpkg.com/d3/-/d3-4.9.1.tgz#f860be9252261a3c14eea64b1d2590d14f4db838"
dependencies:
d3-array "1.2.0"
d3-axis "1.0.7"
d3-brush "1.0.4"
d3-chord "1.0.4"
d3-collection "1.0.3"
d3-color "1.0.3"
d3-dispatch "1.0.3"
d3-drag "1.1.0"
d3-dsv "1.0.5"
d3-ease "1.0.3"
d3-force "1.0.6"
d3-format "1.2.0"
d3-geo "1.6.4"
d3-hierarchy "1.1.4"
d3-interpolate "1.1.5"
d3-path "1.0.5"
d3-polygon "1.0.3"
d3-quadtree "1.0.3"
d3-queue "3.0.7"
d3-random "1.1.0"
d3-request "1.0.5"
d3-scale "1.0.6"
d3-selection "1.1.0"
d3-shape "1.1.1"
d3-time "1.0.6"
d3-time-format "2.0.5"
d3-timer "1.0.5"
d3-transition "1.1.0"
d3-voronoi "1.1.2"
d3-zoom "1.2.0"
d@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
@ -3104,7 +3314,7 @@ https-browserify@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
iconv-lite@0.4.15, iconv-lite@~0.4.13:
iconv-lite@0.4, iconv-lite@0.4.15, iconv-lite@~0.4.13:
version "0.4.15"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
@ -3979,6 +4189,12 @@ methods@~1.1.1, methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
metrics-graphics@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/metrics-graphics/-/metrics-graphics-2.11.0.tgz#1585eb483a0b494f417a65c35a0ade8ece904a72"
dependencies:
d3 "^4"
micromatch@^2.1.5, micromatch@^2.3.11:
version "2.3.11"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
@ -5393,6 +5609,10 @@ run-async@^0.1.0:
dependencies:
once "^1.3.0"
rw@1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
rx-lite@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
@ -6313,6 +6533,10 @@ xmlhttprequest-ssl@1.5.3:
version "1.5.3"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"
xmlhttprequest@1:
version "1.8.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
xtend@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"