706 строки
20 KiB
JavaScript
706 строки
20 KiB
JavaScript
var correlations = (() => {
|
|
let correlationData = {};
|
|
|
|
function sha1(str) {
|
|
return crypto.subtle
|
|
.digest("SHA-1", new TextEncoder("utf-8").encode(str))
|
|
.then((hash) => hex(hash));
|
|
}
|
|
|
|
function hex(buffer) {
|
|
let hexCodes = [];
|
|
let view = new DataView(buffer);
|
|
|
|
for (let i = 0; i < view.byteLength; i += 4) {
|
|
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time).
|
|
let value = view.getUint32(i);
|
|
// toString(16) will give the hex representation of the number without padding.
|
|
let stringValue = value.toString(16);
|
|
// We use concatenation and slice for padding.
|
|
let padding = "00000000";
|
|
let paddedValue = (padding + stringValue).slice(-padding.length);
|
|
hexCodes.push(paddedValue);
|
|
}
|
|
|
|
// Join all the hex strings into one
|
|
return hexCodes.join("");
|
|
}
|
|
|
|
function getDataURL(product) {
|
|
if (product === "Firefox") {
|
|
return "https://analysis-output.telemetry.mozilla.org/top-signatures-correlations/data/";
|
|
} else if (product === "FennecAndroid") {
|
|
return "https://analysis-output.telemetry.mozilla.org/top-fennec-signatures-correlations/data/";
|
|
} else {
|
|
throw new Error("Unknown product: " + product);
|
|
}
|
|
}
|
|
|
|
function loadChannelsData(product) {
|
|
if (correlationData[product]) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return fetch(getDataURL(product) + "all.json.gz")
|
|
.then((response) => response.json())
|
|
.then((totals) => {
|
|
correlationData[product] = {
|
|
date: totals["date"],
|
|
};
|
|
|
|
let channels = ["release", "beta", "nightly"];
|
|
if (product === "Firefox") {
|
|
channels.push("esr");
|
|
}
|
|
|
|
for (let ch of channels) {
|
|
correlationData[product][ch] = {
|
|
total: totals[ch],
|
|
signatures: {},
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadCorrelationData(signature, channel, product) {
|
|
return loadChannelsData(product)
|
|
.then(() => {
|
|
if (signature in correlationData[product][channel]["signatures"]) {
|
|
return;
|
|
}
|
|
|
|
return sha1(signature)
|
|
.then((sha1signature) =>
|
|
fetch(
|
|
getDataURL(product) + channel + "/" + sha1signature + ".json.gz"
|
|
)
|
|
)
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
correlationData[product][channel]["signatures"][signature] = data;
|
|
});
|
|
})
|
|
.catch(() => {})
|
|
.then(() => correlationData);
|
|
}
|
|
|
|
function getAnalysisDate(product) {
|
|
return loadChannelsData(product)
|
|
.then(() => correlationData[product]["date"])
|
|
.catch(() => "");
|
|
}
|
|
|
|
function itemToLabel(item) {
|
|
return Object.getOwnPropertyNames(item)
|
|
.map((key) => key + " = " + item[key])
|
|
.join(" ∧ ");
|
|
}
|
|
|
|
function toPercentage(num) {
|
|
let result = (num * 100).toFixed(2);
|
|
|
|
if (result == "100.00") {
|
|
return "100.0";
|
|
}
|
|
|
|
if (result.substring(0, result.indexOf(".")).length == 1) {
|
|
return "0" + result;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function confidenceInterval(count1, total1, count2, total2) {
|
|
let prop1 = count1 / total1;
|
|
let prop2 = count2 / total2;
|
|
let diff = prop1 - prop2;
|
|
|
|
// Wald 95% confidence interval for the difference between the proportions.
|
|
let standard_error = Math.sqrt(
|
|
(prop1 * (1 - prop1)) / total1 + (prop2 * (1 - prop2)) / total2
|
|
);
|
|
let ci = [diff - 1.96 * standard_error, diff + 1.96 * standard_error];
|
|
|
|
// Yates continuity correction for the confidence interval.
|
|
let correction = 0.5 * (1.0 / total1 + 1.0 / total2);
|
|
|
|
return [ci[0] - correction, ci[1] + correction];
|
|
}
|
|
|
|
function sortCorrelationData(correlationData, total_reference, total_group) {
|
|
return correlationData.sort((a, b) => {
|
|
let rule_a_len = Object.keys(a.item).length;
|
|
let rule_b_len = Object.keys(b.item).length;
|
|
|
|
if (rule_a_len < rule_b_len) {
|
|
return -1;
|
|
}
|
|
|
|
if (rule_a_len > rule_b_len) {
|
|
return 1;
|
|
}
|
|
|
|
// Then, sort by percentage difference between signature and
|
|
// overall (using the lower endpoint of the confidence interval
|
|
// of the difference).
|
|
let ciA = null;
|
|
if (a.prior) {
|
|
// If one of the two elements has a prior that alters a rule's
|
|
// distribution significantly, sort by the percentage of the rule
|
|
// given the prior.
|
|
ciA = confidenceInterval(
|
|
a.prior.count_group,
|
|
a.prior.total_group,
|
|
a.prior.count_reference,
|
|
a.prior.total_reference
|
|
);
|
|
} else {
|
|
ciA = confidenceInterval(
|
|
a.count_group,
|
|
total_group,
|
|
a.count_reference,
|
|
total_reference
|
|
);
|
|
}
|
|
|
|
let ciB = null;
|
|
if (b.prior) {
|
|
ciB = confidenceInterval(
|
|
b.prior.count_group,
|
|
b.prior.total_group,
|
|
b.prior.count_reference,
|
|
b.prior.total_reference
|
|
);
|
|
} else {
|
|
ciB = confidenceInterval(
|
|
b.count_group,
|
|
total_group,
|
|
b.count_reference,
|
|
total_reference
|
|
);
|
|
}
|
|
|
|
return (
|
|
Math.min(Math.abs(ciB[0]), Math.abs(ciB[1])) -
|
|
Math.min(Math.abs(ciA[0]), Math.abs(ciA[1]))
|
|
);
|
|
});
|
|
}
|
|
|
|
function itemEqual(item1, item2) {
|
|
let keys1 = Object.keys(item1);
|
|
let keys2 = Object.keys(item2);
|
|
|
|
if (keys1.length !== keys2.length) {
|
|
return false;
|
|
}
|
|
|
|
for (let prop of keys1.concat(keys2)) {
|
|
let val1 = item1[prop];
|
|
let val2 = item2[prop];
|
|
|
|
if (typeof val1 === "string") {
|
|
val1 = val1.toLowerCase();
|
|
}
|
|
|
|
if (typeof val2 === "string") {
|
|
val2 = val2.toLowerCase();
|
|
}
|
|
|
|
if (item1[prop] !== item2[prop]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
let channelsData = {};
|
|
|
|
function loadChannelsDifferencesData(product) {
|
|
return fetch(
|
|
"https://analysis-output.telemetry.mozilla.org/channels-differences/data/differences.json.gz"
|
|
)
|
|
.then((response) => response.json())
|
|
.then((data) => (channelsData = data));
|
|
}
|
|
|
|
function socorroToTelemetry(socorroKey, socorroValue) {
|
|
let valueMapping = {
|
|
cpu_arch: {
|
|
values: {
|
|
amd64: "x86-64",
|
|
},
|
|
},
|
|
os_arch: {
|
|
values: {
|
|
amd64: "x86-64",
|
|
},
|
|
},
|
|
platform: {
|
|
key: "os_name",
|
|
values: {
|
|
"Mac OS X": "Darwin",
|
|
"Windows NT": "Windows_NT",
|
|
},
|
|
},
|
|
platform_version: {
|
|
key: "os_version",
|
|
},
|
|
platform_pretty_version: {
|
|
key: "os_pretty_version",
|
|
values: {
|
|
"Windows 10": "10.0",
|
|
"Windows 8.1": "6.3",
|
|
"Windows 8": "6.2",
|
|
"Windows 7": "6.1",
|
|
"Windows Server 2003": "5.2",
|
|
"Windows XP": "5.1",
|
|
"Windows 2000": "5.0",
|
|
"Windows NT": "4.0",
|
|
},
|
|
},
|
|
e10s_enabled: {
|
|
key: "e10s_enabled",
|
|
values: {
|
|
1: true,
|
|
},
|
|
},
|
|
dom_ipc_enabled: {
|
|
key: "e10s_enabled",
|
|
values: {
|
|
1: true,
|
|
},
|
|
},
|
|
'"D2D1.1+" in app_notes': {
|
|
key: "d2d_enabled",
|
|
},
|
|
'"D2D1.1-" in app_notes': {
|
|
key: "d2d_enabled",
|
|
values: (v) => !v,
|
|
},
|
|
'"DWrite+" in app_notes': {
|
|
key: "d_write_enabled",
|
|
},
|
|
'"DWrite-" in app_notes': {
|
|
key: "d_write_enabled",
|
|
values: (v) => !v,
|
|
},
|
|
adapter_vendor_id: {
|
|
values: {
|
|
"NVIDIA Corporation": "0x10de",
|
|
"Intel Corporation": "0x8086",
|
|
},
|
|
},
|
|
"CPU Info": {
|
|
key: "cpu_info",
|
|
},
|
|
};
|
|
|
|
let key, value;
|
|
if (socorroKey in valueMapping) {
|
|
let mapping = valueMapping[socorroKey];
|
|
key = mapping["key"] || socorroKey;
|
|
if (mapping["values"]) {
|
|
if (typeof mapping["values"] === "function") {
|
|
value = mapping["values"](socorroValue);
|
|
} else {
|
|
value = mapping["values"][socorroValue];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof key === "undefined") {
|
|
key = socorroKey;
|
|
}
|
|
|
|
if (typeof value === "undefined") {
|
|
value = socorroValue;
|
|
}
|
|
|
|
return [key, value];
|
|
}
|
|
|
|
function getChannelPercentage(channel, socorroItem) {
|
|
// console.log('socorro')
|
|
// console.log(socorroItem)
|
|
|
|
let translatedItem = {};
|
|
for (let prop of Object.keys(socorroItem)) {
|
|
let [telemetryProp, telemetryValue] = socorroToTelemetry(
|
|
prop,
|
|
socorroItem[prop]
|
|
);
|
|
//console.log(telemetryProp + ' - ' + telemetryValue)
|
|
translatedItem[telemetryProp] = telemetryValue;
|
|
}
|
|
|
|
// console.log('translated')
|
|
// console.log(translatedItem)
|
|
|
|
let found = channelsData[channel].find((longitudinalElem) =>
|
|
itemEqual(longitudinalElem.item, translatedItem)
|
|
);
|
|
if (!found) {
|
|
return 0;
|
|
}
|
|
|
|
// console.log('telemetry ' + channel)
|
|
// console.log(found)
|
|
|
|
/*console.log('cpu_info');
|
|
console.log(channelsData[channel].filter(longitudinalElem => Object.keys(longitudinalElem.item).indexOf('cpu_info') != -1));*/
|
|
|
|
return found.p;
|
|
}
|
|
|
|
function rerank(textElem, signature, channel, channel_target, product) {
|
|
textElem.textContent = "";
|
|
|
|
return loadChannelsDifferencesData(product)
|
|
.then(() => loadCorrelationData(signature, channel, product))
|
|
.then((data) => {
|
|
if (!(product in data)) {
|
|
textElem.textContent =
|
|
"No correlation data was generated for the '" +
|
|
product +
|
|
"' product.";
|
|
return [];
|
|
}
|
|
|
|
if (
|
|
!(signature in data[product][channel]["signatures"]) ||
|
|
!data[product][channel]["signatures"][signature]["results"]
|
|
) {
|
|
textElem.textContent =
|
|
'No correlation data was generated for the signature "' +
|
|
signature +
|
|
'" on the ' +
|
|
channel +
|
|
" channel, for the '" +
|
|
product +
|
|
"' product.";
|
|
return [];
|
|
}
|
|
|
|
let correlationData =
|
|
data[product][channel]["signatures"][signature]["results"];
|
|
|
|
let total_reference = data[product][channel].total;
|
|
let total_group = data[product][channel]["signatures"][signature].total;
|
|
|
|
return correlationData
|
|
.filter((socorroElem) => Object.keys(socorroElem.item).length == 1)
|
|
.filter(
|
|
(socorroElem) =>
|
|
getChannelPercentage(channel, socorroElem.item) != 0
|
|
)
|
|
.sort((a, b) => {
|
|
//return getChannelPercentage(channel_target, b.item) / getChannelPercentage(channel, b.item) - getChannelPercentage(channel_target, a.item) / getChannelPercentage(channel, a.item);
|
|
return b.count_group / total_group - a.count_group / total_group;
|
|
})
|
|
.map((elem) => {
|
|
return {
|
|
property: itemToLabel(elem.item),
|
|
in_signature: toPercentage(elem.count_group / total_group),
|
|
in_channel_target: getChannelPercentage(
|
|
channel_target,
|
|
elem.item
|
|
),
|
|
in_channel: getChannelPercentage(channel, elem.item),
|
|
};
|
|
});
|
|
});
|
|
}
|
|
|
|
function getChannelsProperties(product) {
|
|
return loadChannelsDifferencesData(product)
|
|
.then(() =>
|
|
["release", "beta", "nightly"].map((channel) =>
|
|
channelsData[channel].map(
|
|
(longitudinalElem) => Object.keys(longitudinalElem.item)[0]
|
|
)
|
|
)
|
|
)
|
|
.then((all_props_per_channel) =>
|
|
[].concat.apply([], all_props_per_channel)
|
|
)
|
|
.then((all_props) => new Set(all_props))
|
|
.then((props) => Array.from(props));
|
|
}
|
|
|
|
function getChannelsValues(product, property) {
|
|
return loadChannelsDifferencesData(product)
|
|
.then(() =>
|
|
["release", "beta", "nightly"].map((channel) =>
|
|
channelsData[channel]
|
|
.filter(
|
|
(longitudinalElem) =>
|
|
Object.keys(longitudinalElem.item).indexOf(property) != -1
|
|
)
|
|
.map((longitudinalElem) => longitudinalElem.item[property])
|
|
)
|
|
)
|
|
.then((all_values_per_channel) =>
|
|
[].concat.apply([], all_values_per_channel)
|
|
)
|
|
.then((all_values) => new Set(all_values))
|
|
.then((values) => Array.from(values));
|
|
}
|
|
|
|
function getChannelsPercentage(product, channel, property, value) {
|
|
return loadChannelsDifferencesData(product).then(() =>
|
|
channelsData[channel].find(
|
|
(longitudinalElem) =>
|
|
Object.keys(longitudinalElem.item).indexOf(property) != -1 &&
|
|
longitudinalElem.item[property] == value
|
|
)
|
|
);
|
|
}
|
|
|
|
function text(textElem, signature, channel, product, show_ci = false) {
|
|
loadCorrelationData(signature, channel, product).then((data) => {
|
|
textElem.textContent = "";
|
|
|
|
if (!(product in data)) {
|
|
textElem.textContent =
|
|
"No correlation data was generated for the '" +
|
|
product +
|
|
"' product.";
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!(signature in data[product][channel]["signatures"]) ||
|
|
!data[product][channel]["signatures"][signature]["results"]
|
|
) {
|
|
textElem.textContent =
|
|
'No correlation data was generated for the signature "' +
|
|
signature +
|
|
'" on the ' +
|
|
channel +
|
|
" channel, for the '" +
|
|
product +
|
|
"' product.";
|
|
return;
|
|
}
|
|
|
|
let correlationData =
|
|
data[product][channel]["signatures"][signature]["results"];
|
|
|
|
let total_reference = data[product][channel].total;
|
|
let total_group = data[product][channel]["signatures"][signature].total;
|
|
|
|
textElem.textContent = sortCorrelationData(
|
|
correlationData,
|
|
total_reference,
|
|
total_group
|
|
).reduce((prev, cur) => {
|
|
let support_group = toPercentage(cur.count_group / total_group);
|
|
let support_reference = toPercentage(
|
|
cur.count_reference / total_reference
|
|
);
|
|
let support_diff = toPercentage(
|
|
Math.abs(
|
|
cur.count_group / total_group -
|
|
cur.count_reference / total_reference
|
|
)
|
|
);
|
|
|
|
let ci = confidenceInterval(
|
|
cur.count_group,
|
|
total_group,
|
|
cur.count_reference,
|
|
total_reference
|
|
);
|
|
|
|
let support_diff_incertezza = toPercentage(
|
|
Math.abs(
|
|
Math.abs(ci[0]) -
|
|
Math.abs(
|
|
cur.count_group / total_group -
|
|
cur.count_reference / total_reference
|
|
)
|
|
)
|
|
);
|
|
|
|
let res =
|
|
prev +
|
|
"(" +
|
|
support_group +
|
|
"% in signature vs " +
|
|
support_reference +
|
|
"% overall, difference " +
|
|
support_diff +
|
|
"±" +
|
|
support_diff_incertezza +
|
|
"%) " +
|
|
itemToLabel(cur.item);
|
|
|
|
if (cur.prior) {
|
|
let support_group_given_prior = toPercentage(
|
|
cur.prior.count_group / cur.prior.total_group
|
|
);
|
|
let support_reference_given_prior = toPercentage(
|
|
cur.prior.count_reference / cur.prior.total_reference
|
|
);
|
|
res +=
|
|
" [" +
|
|
support_group_given_prior +
|
|
"% vs " +
|
|
support_reference_given_prior +
|
|
"% if " +
|
|
itemToLabel(cur.prior.item) +
|
|
"]";
|
|
}
|
|
|
|
return res + "\n";
|
|
}, "");
|
|
|
|
textElem.textContent +=
|
|
"\n\nTop Words: " +
|
|
data[product][channel]["signatures"][signature]["top_words"].join(", ");
|
|
});
|
|
}
|
|
|
|
function graph(svgElem, signature, channel, product) {
|
|
loadCorrelationData(signature, channel, product).then((data) => {
|
|
d3.select(svgElem).selectAll("*").remove();
|
|
|
|
if (
|
|
!(product in data) ||
|
|
!(signature in data[product][channel]["signatures"]) ||
|
|
!data[product][channel]["signatures"][signature]["results"]
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let total_reference = data[product][channel].total;
|
|
let total_group = data[product][channel]["signatures"][signature].total;
|
|
|
|
let correlationData = data[product][channel]["signatures"][signature][
|
|
"results"
|
|
].filter((elem) => Object.keys(elem.item).length <= 1);
|
|
correlationData = sortCorrelationData(
|
|
correlationData,
|
|
total_reference,
|
|
total_group
|
|
);
|
|
correlationData.reverse();
|
|
|
|
let margin = { top: 20, right: 300, bottom: 30, left: 300 };
|
|
let width = svgElem.getAttribute("width") - margin.left - margin.right;
|
|
let height = svgElem.getAttribute("height") - margin.top - margin.bottom;
|
|
|
|
let y0 = d3.scale.ordinal().rangeRoundBands([height, 0], 0.2, 0.5);
|
|
|
|
let y1 = d3.scale.ordinal();
|
|
|
|
let x = d3.scale.linear().range([0, width]);
|
|
|
|
let color = d3.scale.ordinal().range(["blue", "red"]);
|
|
|
|
let xAxis = d3.svg.axis().scale(x).tickSize(-height).orient("bottom");
|
|
|
|
let yAxis = d3.svg.axis().scale(y0).orient("left");
|
|
|
|
let svg = d3
|
|
.select(svgElem)
|
|
.attr("width", width + margin.left + margin.right)
|
|
.attr("height", height + margin.top + margin.bottom)
|
|
.append("g")
|
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
|
|
|
let options = [signature, "Overall"];
|
|
|
|
correlationData.forEach((d) => {
|
|
d.values = [
|
|
{ name: "Overall", value: d.count_reference / total_reference },
|
|
{ name: signature, value: d.count_group / total_group },
|
|
];
|
|
});
|
|
|
|
y0.domain(correlationData.map((d) => itemToLabel(d.item)));
|
|
y1.domain(options).rangeRoundBands([0, y0.rangeBand()]);
|
|
x.domain([0, 100]);
|
|
|
|
svg
|
|
.append("g")
|
|
.attr("class", "x axis")
|
|
.attr("transform", "translate(0," + height + ")")
|
|
.call(xAxis);
|
|
|
|
svg.append("g").attr("class", "y axis").call(yAxis);
|
|
|
|
let bar = svg
|
|
.selectAll(".bar")
|
|
.data(correlationData)
|
|
.enter()
|
|
.append("g")
|
|
.attr("class", "rect")
|
|
.attr(
|
|
"transform",
|
|
(d) => "translate( 0," + y0(itemToLabel(d.item)) + ")"
|
|
);
|
|
|
|
let bar_enter = bar
|
|
.selectAll("rect")
|
|
.data((d) => d.values)
|
|
.enter();
|
|
|
|
bar_enter
|
|
.append("rect")
|
|
.attr("height", y1.rangeBand())
|
|
.attr("y", (d) => y1(d.name))
|
|
.attr("x", (d) => 0)
|
|
.attr("value", (d) => d.name)
|
|
.attr("width", (d) => x((d.value * 100).toFixed(2)))
|
|
.style("fill", (d) => color(d.name));
|
|
|
|
bar_enter
|
|
.append("text")
|
|
.attr("x", (d) => x((d.value * 100).toFixed(2)) + 5)
|
|
.attr("y", (d) => y1(d.name) + y1.rangeBand() / 2)
|
|
.attr("dy", ".35em")
|
|
.text((d) => (d.value * 100).toFixed(2));
|
|
|
|
let legend = svg
|
|
.selectAll(".legend")
|
|
.data(options.slice())
|
|
.enter()
|
|
.append("g")
|
|
.attr("class", "legend")
|
|
.attr(
|
|
"transform",
|
|
(d, i) => "translate(" + margin.right + "," + i * 20 + ")"
|
|
);
|
|
|
|
legend
|
|
.append("rect")
|
|
.attr("x", width - 18)
|
|
.attr("width", 18)
|
|
.attr("height", 18)
|
|
.style("fill", color);
|
|
|
|
legend
|
|
.append("text")
|
|
.attr("x", width - 24)
|
|
.attr("y", 9)
|
|
.attr("dy", ".35em")
|
|
.style("text-anchor", "end")
|
|
.text((d) => d);
|
|
});
|
|
}
|
|
|
|
return {
|
|
getAnalysisDate: getAnalysisDate,
|
|
text: text,
|
|
rerank: rerank,
|
|
getChannelsProperties: getChannelsProperties,
|
|
getChannelsValues: getChannelsValues,
|
|
getChannelsPercentage: getChannelsPercentage,
|
|
toPercentage: toPercentage,
|
|
graph: graph,
|
|
};
|
|
})();
|