blob: 2ab318e5a02fd2728aa6f9455eaca1d00d78083a [file] [log] [blame]
'use strict';
const MiB = 1024 * 1024;
Vue.component('v-select', VueSelect.VueSelect);
const menu = new Vue({
el: '#menu',
data: {
state: {
browser: null,
subprocess: null,
component: null,
size: null,
subcomponent: null,
subsubcomponent: null,
testResults: [],
referenceColumn: '',
},
browserOptions: [],
subprocessOptions: [],
metricNames: null,
sampleArr: null,
guidValueInfo: null,
componentMap: null,
sizeMap: null,
allLabels: [],
significanceTester: new MetricSignificance(),
},
mounted() {
app.$on('stack_clicked', this.splitMemoryMetric);
},
computed: {
// Compute size options. The user will be provided with all
// sizes and the probe will be auto detected from it.
sizeOptions() {
if (this.sizeMap === null) {
return undefined;
}
let sizes = [];
for (const [key, value] of this.sizeMap.entries()) {
sizes = sizes.concat(value);
}
return sizes;
},
// The components are different depending on the type of probe.
// The probe is auto detected depending on the chosen size.
// Then the user is provided with the first level of components.
componentsOptions() {
if (this.componentMap === null || this.state.size === null) {
return undefined;
}
for (const [key, value] of this.sizeMap.entries()) {
if (value.includes(this.state.size)) {
this.probe = key;
}
}
const components = [];
for (const [key, value] of this.componentMap.get(this.probe).entries()) {
components.push(key);
}
return components;
},
// Compute the options for the first subcomponent depending on the probes.
// When the user chooses a component, it might be a hierarchical one.
firstSubcompOptions() {
if (this.state.component === null) {
return undefined;
}
const subcomponent = [];
for (const [key, value] of this
.componentMap.get(this.probe).get(this.state.component).entries()) {
subcomponent.push(key);
}
return subcomponent;
},
// In case when the component is from Chrome, the hierarchy might have more
// levels.
secondSubcompOptions() {
if (this.state.subcomponent === null) {
return undefined;
}
const subcomponent = [];
for (const [key, value] of this
.componentMap
.get(this.probe)
.get(this.state.component)
.get(this.state.subcomponent).entries()) {
subcomponent.push(key);
}
return subcomponent;
}
},
watch: {
'state.size'() {
this.state.component = null;
this.state.subcomponent = null;
this.state.subsubcomponent = null;
},
'state.component'() {
this.state.subcomponent = null;
this.state.subsubcomponent = null;
},
'state.subcomponent'() {
this.state.subsubcomponent = null;
},
'state.referenceColumn'() {
if (this.state.referenceColumn === null) {
this.state.testResults = [];
return;
}
this.significanceTester.referenceColumn = this.state.referenceColumn;
this.state.testResults = this.significanceTester.mostSignificant();
}
},
methods: {
// Build the available metrics upon the chosen items.
// The method applies an intersection for all of them and
// return the result as a collection of metrics that matched.
// Also the metric that exactly matches the menu selected items
// will be the first one in array and the first row in table.
apply() {
let nameOfMetric = 'memory:' +
this.state.browser + ':' +
this.state.subprocess + ':' +
this.probe + ':' +
this.state.component;
if (this.state.subcomponent !== null) {
nameOfMetric += ':' + this.state.subcomponent;
}
if (this.state.subsubcomponent !== null) {
nameOfMetric += ':' + this.state.subsubcomponent;
}
let metrics = [];
for (const name of this.metricNames) {
if (this.state.browser !== null &&
name.includes(this.state.browser) &&
this.state.subprocess !== null &&
name.includes(this.state.subprocess) &&
this.state.component !== null &&
name.includes(this.state.component) &&
this.state.size !== null &&
name.includes(this.state.size) &&
this.probe !== null &&
name.includes(this.probe)) {
if (this.state.subcomponent === null) {
metrics.push(name);
} else {
if (name.includes(this.state.subcomponent)) {
if (this.state.subsubcomponent === null) {
metrics.push(name);
} else {
if (name.includes(this.state.subsubcomponent)) {
metrics.push(name);
}
}
}
}
}
}
nameOfMetric += ':' + this.state.size;
if (_.uniq(metrics).length === 0) {
alert('No metrics found');
} else {
metrics = _.uniq(metrics);
metrics.splice(metrics.indexOf(nameOfMetric), 1);
metrics.unshift(nameOfMetric);
app.state.parsedMetrics = metrics;
}
},
/**
* Splits a memory metric into it's heirarchical data and
* assigns this heirarchy information into the relavent fields
* of the menu. It also updates the table to display only the
* given metric.
* @param {string} metricName The name of the metric to be split.
*/
async splitMemoryMetric(metricName) {
if (!metricName.startsWith('memory')) {
throw new Error('Expected a memory metric');
}
const parts = metricName.split(':');
const heirarchyInformation = {
browser: 1,
process: 2,
probe: 3,
componentStart: 4,
// Metrics have a variable number of subcomponents
// (.e.g, a metric which is an aggregate over subcomponents will
// have one less subcomponent field than it's sub-metrics).
// Therefore, the end of the subcomponents field and
// location of the size field must be calculated dynamically.
componentsEnd: parts.length - 2,
size: parts.length - 1,
};
// Assigning to these fields updates the corresponding select
// menu in the UI.
this.state.browser = parts[heirarchyInformation.browser];
this.state.subprocess = parts[heirarchyInformation.process];
this.probe = parts[heirarchyInformation.probe];
this.state.size = parts[heirarchyInformation.size];
// The size watcher sets 'this.state.component' to null so we must wait
// for the DOM to be updated. Then the size watcher is called before
// assigning to 'this.state.component' and so it is not overwritten
// with null.
await this.$nextTick();
this.state.component = parts[heirarchyInformation.componentStart];
const start = heirarchyInformation.componentStart;
const end = heirarchyInformation.componentsEnd;
for (let i = start + 1; i <= end; i++) {
const subcomponent = i - heirarchyInformation.componentStart;
switch (subcomponent) {
case 1: {
// See above comment (component watcher sets subcomponent to null).
await this.$nextTick();
this.state.subcomponent = parts[i];
break;
}
case 2: {
// See above comment
// (subcomponent watcher sets subsubcomponent to null).
await this.$nextTick();
this.state.subsubcomponent = parts[i];
break;
}
default: throw new Error('Unexpected number of subcomponents.');
}
}
app.state.parsedMetrics = [metricName];
},
}
});
function average(arr) {
return _.reduce(arr, function(memo, num) {
return memo + num;
}, 0) / arr.length;
}
// This function returns an object containing:
// all the names of labels plus a map like this:
// map: [metric_name] -> [map: [lable] -> sampleValue],
// where the lable is each name of all sub-labels
// and sampleValue is the average for a specific
// metric across stories with a specific label.
function getMetricStoriesLabelsToValuesMap(sampleArr, guidValueInfo) {
const newDiagnostics = new Set();
const metricToDiagnosticValuesMap = new Map();
for (const elem of sampleArr) {
let currentDiagnostic = guidValueInfo.
get(elem.diagnostics.labels);
if (currentDiagnostic === undefined) {
continue;
}
if (currentDiagnostic !== 'number') {
currentDiagnostic = currentDiagnostic[0];
}
newDiagnostics.add(currentDiagnostic);
if (!metricToDiagnosticValuesMap.has(elem.name)) {
const map = new Map();
map.set(currentDiagnostic, [average(elem.sampleValues)]);
metricToDiagnosticValuesMap.set(elem.name, map);
} else {
const map = metricToDiagnosticValuesMap.get(elem.name);
if (map.has(currentDiagnostic)) {
const array = map.get(currentDiagnostic);
array.push(average(elem.sampleValues));
map.set(currentDiagnostic, array);
metricToDiagnosticValuesMap.set(elem.name, map);
} else {
map.set(currentDiagnostic, [average(elem.sampleValues)]);
metricToDiagnosticValuesMap.set(elem.name, map);
}
}
}
return {
labelNames: Array.from(newDiagnostics),
mapLabelToValues: metricToDiagnosticValuesMap
};
}
function fromBytesToMiB(value) {
return (value / MiB).toFixed(5);
}
// Load the content of the file and further display the data.
function readSingleFile(e) {
const file = e.target.files[0];
if (!file) {
return;
}
// Extract data from file and distribute it in some relevant structures:
// results for all guid-related( for now they are not
// divided in 3 parts depending on the type ) and
// all results with sample-value-related and
// map guid to value within the same structure
const reader = new FileReader();
reader.onload = function(e) {
const contents = extractData(e.target.result);
const sampleArr = contents.sampleValueArray;
const guidValueInfo = contents.guidValueInfo;
const metricAverage = new Map();
const allLabels = new Set();
for (const e of sampleArr) {
// This version of the tool focuses on analysing memory
// metrics, which contain a slightly different structure
// to the non-memory metrics.
if (e.name.startsWith('memory')) {
const { name, sampleValues, diagnostics } = e;
const { labels, stories } = diagnostics;
const label = guidValueInfo.get(labels)[0];
allLabels.add(label);
const story = guidValueInfo.get(stories)[0];
menu.significanceTester.add(name, label, story, sampleValues);
}
}
menu.allLabels = Array.from(allLabels);
let metricNames = [];
sampleArr.map(e => metricNames.push(e.name));
metricNames = _.uniq(metricNames);
// The content for the default table: with name
// of the metric, the average value of the sample values
// plus an id. The latest is used to expand the row.
// It may disappear later.
const tableElems = [];
let id = 1;
for (const name of metricNames) {
tableElems.push({
id: id++,
metric: name
});
}
const labelsResult = getMetricStoriesLabelsToValuesMap(
sampleArr, guidValueInfo);
const columnsForChosenDiagnostic = labelsResult.labelNames;
const metricToDiagnosticValuesMap = labelsResult.mapLabelToValues;
for (const elem of tableElems) {
if (metricToDiagnosticValuesMap.get(elem.metric) === undefined) {
continue;
}
for (const diagnostic of columnsForChosenDiagnostic) {
if (!metricToDiagnosticValuesMap.get(elem.metric).has(diagnostic)) {
continue;
}
elem[diagnostic] = fromBytesToMiB(average(metricToDiagnosticValuesMap
.get(elem.metric).get(diagnostic)));
}
}
app.state.gridData = tableElems;
app.defaultGridData = tableElems;
app.sampleArr = sampleArr;
app.guidValue = guidValueInfo;
app.columnsForChosenDiagnostic = columnsForChosenDiagnostic;
const result = parseAllMetrics(metricNames);
menu.sampelArr = sampleArr;
menu.guidValueInfo = guidValueInfo;
menu.browserOptions = result.browsers;
menu.subprocessOptions = result.subprocesses;
menu.componentMap = result.components;
menu.sizeMap = result.sizes;
menu.metricNames = result.names;
};
reader.readAsText(file);
}
function extractData(contents) {
/*
* Populate guidValue with guidValue objects containing
* guid and value from the same type of data.
*/
const guidValueInfoMap = new Map();
const result = [];
const sampleValue = [];
const dateRangeMap = new Map();
const other = [];
/*
* Extract every piece of data between <histogram-json> tags;
* all data is written between these tags
*/
const reg = /<histogram-json>(.*?)<\/histogram-json>/g;
let m = reg.exec(contents);
while (m !== null) {
result.push(m[1]);
m = reg.exec(contents);
}
for (const element of result) {
const e = JSON.parse(element);
if (e.hasOwnProperty('sampleValues')) {
const elem = {
name: e.name,
sampleValues: e.sampleValues,
unit: e.unit,
guid: e.guid,
diagnostics: {}
};
if (e.diagnostics === undefined || e.diagnostics === null) {
continue;
}
if (e.diagnostics.hasOwnProperty('traceUrls')) {
elem.diagnostics.traceUrls = e.diagnostics.traceUrls;
}
if (e.diagnostics.hasOwnProperty('labels')) {
elem.diagnostics.labels = e.diagnostics.labels;
} else {
elem.diagnostics.labels = e.diagnostics.benchmarkStart;
}
if (e.diagnostics.hasOwnProperty('stories')) {
elem.diagnostics.stories = e.diagnostics.stories;
}
if (e.diagnostics.hasOwnProperty('storysetRepeats')) {
elem.diagnostics.storysetRepeats = e.diagnostics.storysetRepeats;
}
sampleValue.push(elem);
} else {
if (e.type === 'GenericSet') {
guidValueInfoMap.set(e.guid, e.values);
} else if (e.type === 'DateRange') {
guidValueInfoMap.set(e.guid, e.min);
} else {
other.push(e);
}
}
}
return {
guidValueInfo: guidValueInfoMap,
guidMinInfo: dateRangeMap,
otherTypes: other,
sampleValueArray: sampleValue
};
}
document.getElementById('file-input')
.addEventListener('change', readSingleFile, false);