correlations.js (589 lines of code) (raw):

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, }; })();