Bug 1654384 - Add a specific simulator to each of the distributions metrics on the book (#1150)

This commit is contained in:
Beatriz Rizental 2020-08-19 10:19:43 +02:00 коммит произвёл GitHub
Родитель 15bd281d3a
Коммит eda8c4c686
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 1085 добавлений и 2 удалений

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

@ -9,7 +9,7 @@ create-missing = false
[output.html]
additional-css = ["glean.css", "mermaid.css"]
additional-js = ["tabs.js", "mermaid.min.js", "mermaid-init.js"]
additional-js = ["tabs.js", "mermaid.min.js", "mermaid-init.js", "chart.min.js", "chart-distributions.js", "chart-distributions-ui.js"]
git-repository-url = "https://github.com/mozilla/glean"
git-branch = "main"
mathjax-support = true

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

@ -0,0 +1,173 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
"use strict"
if (document.getElementById("histogram-chart")) {
// Transformation function we may want to apply to each value before plotting
//
// The current use cases are memory distributions and timing distribution,
// which may receive the values in a given unit, but transform them to a base one upon recording
let TRANSFORMATION;
function memoryUnitToByte(unit) {
switch(unit) {
case "byte":
return value => value;
case "kilobyte":
return value => value * Math.pow(2, 10);
case "megabyte":
return value => value * Math.pow(2, 20);
case "gigabyte":
return value => value * Math.pow(2, 30);
}
}
const memoryUnitSelect = document.querySelector("#histogram-props select#memory-unit")
if (memoryUnitSelect) {
setInputValueFromSearchParam(memoryUnitSelect);
TRANSFORMATION = memoryUnitToByte(memoryUnitSelect.value);
memoryUnitSelect.addEventListener("change", event => {
let memoryUnit = event.target.value;
TRANSFORMATION = memoryUnitToByte(memoryUnit);
let input = event.target;
setURLSearchParam(input.name, input.value);
buildChartFromInputs();
})
}
function timeUnitToNanos(unit) {
switch(unit) {
case "nanoseconds":
return value => value;
case "microseconds":
return value => value * 1000;
case "milliseconds":
return value => value * 1000 * 1000;
}
}
const baseMax = 1000 * 1000 * 1000 * 60 * 10;
const timeUnitToMaxValue = {
"nanoseconds": baseMax,
"microseconds": timeUnitToNanos("microseconds")(baseMax),
"milliseconds": timeUnitToNanos("milliseconds")(baseMax),
};
const timeUnitSelect = document.querySelector("#histogram-props select#time-unit");
const maxValueInput = document.getElementById("maximum-value");
if (timeUnitSelect) {
setInputValueFromSearchParam(timeUnitSelect);
TRANSFORMATION = timeUnitToNanos(timeUnitSelect.value);
timeUnitSelect.addEventListener("change", event => {
let timeUnit = event.target.value;
maxValueInput.value = timeUnitToMaxValue[timeUnit];
TRANSFORMATION = timeUnitToNanos(timeUnit);
let input = event.target;
setURLSearchParam(input.name, input.value);
buildChartFromInputs();
})
}
// Open custom data modal when custom data option is selected
const customDataInput = document.getElementById("custom-data-input-group");
customDataInput.addEventListener('click', () => {
customDataModalOverlay.style.display = "block";
const customDataTextarea = document.querySelector("#custom-data-modal textarea");
if (!customDataTextarea.value) fillUpTextareaWithDummyData(customDataTextarea);
})
// Rebuild chart everytime the custom data text is changed
const customDataTextarea = document.querySelector("#custom-data-modal textarea");
customDataTextarea.addEventListener("change", () => buildChartFromInputs());
// Close modal when we click the overlay
const customDataModalOverlay = document.getElementById("custom-data-modal-overlay");
customDataModalOverlay && customDataModalOverlay.addEventListener('click', () => {
customDataModalOverlay.style.display = "none";
});
// We need to stop propagation for click events on the actual modal,
// so that clicking it doesn't close it
const customDataModal = document.getElementById("custom-data-modal");
customDataModal.addEventListener("click", event => event.stopPropagation());
const options = document.querySelectorAll("#data-options input");
options.forEach(option => {
option.addEventListener("change", event => {
event.preventDefault();
let input = event.target;
setURLSearchParam(input.name, input.value);
buildChartFromInputs();
});
if (searchParams().get(option.name) == option.value) {
option.checked = true;
// We won't save the custom data in the URL,
// if that is the value on load, we create dummy data
if (option.value == "custom") {
const customDataTextarea = document.querySelector("#custom-data-modal textarea");
fillUpTextareaWithDummyData(customDataTextarea);
}
}
});
const inputs = [
...document.querySelectorAll("#histogram-props input"),
document.querySelector("#histogram-props select#kind")
];
inputs.forEach(input => {
setInputValueFromSearchParam(input);
input.addEventListener("change", event => {
let input = event.target;
setURLSearchParam(input.name, input.value);
buildChartFromInputs();
});
});
buildChartFromInputs();
/**
* Build and replace the previous chart with a new one, based on the page inputs.
*/
function buildChartFromInputs() {
const kind = document.getElementById("kind").value
let props;
if (kind == "functional") {
const logBase = Number(document.getElementById("log-base").value);
const bucketsPerMagnitude = Number(document.getElementById("buckets-per-magnitude").value);
const maximumValue = Number(document.getElementById("maximum-value").value || Number.MAX_SAFE_INTEGER);
props = {
logBase,
bucketsPerMagnitude,
maximumValue
}
} else {
const lowerBound = Number(document.getElementById("lower-bound").value);
const upperBound = Number(document.getElementById("upper-bound").value);
const bucketCount = Number(document.getElementById("bucket-count").value);
props = {
lowerBound,
upperBound,
bucketCount
}
}
buildChart(
kind,
props,
document.querySelector("#data-options input:checked").value,
document.querySelector("#custom-data-modal textarea").value,
document.getElementById("histogram-chart-legend"),
document.getElementById("histogram-chart"),
TRANSFORMATION
)
}
}

589
docs/chart-distributions.js Normal file
Просмотреть файл

@ -0,0 +1,589 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
"use strict"
// Do not show dataset legend for graph,
// defining this in the Chart options doesn't seem to work
Chart.defaults.global.legend = false;
const DATA_SAMPLE_COUNT = 20000;
/**
* Build and replace the previous chart with a new one.
*
* @param {String} kind The kind of histogram that should be build, possible values are "functional", "exponential" or "linear"
* @param {Object} props The properties related to the given histogram, keys differ based in the kind
* @param {String} dataOption The chosen way to build data, possible values are "normally-distributed", "log-normally-distributed", "uniformly-distributed" or "custom"
* @param {String} customData In case `dataOption` is "custom", this should contain a String containing a JSON array of numbers
* @param {HTMLElement} legend The HTML element that should contain the text of the chart legend
* @param {HTMLElement} chartSpace The HTML element that should contain the chart
* @param {function} transformation Option function to be applied to generated values
*/
function buildChart (kind, props, dataOption, customData, chartLegend, chartSpace, transformation) {
const { buckets, data, percentages, mean } = buildData(kind, props, dataOption, customData, transformation);
if (kind != "functional") {
chartLegend.innerHTML = `Using these parameters, the widest bucket's width is <b>${getWidestBucketWidth(buckets)}</b>.`;
} else {
chartLegend.innerHTML = `
Using these parameters, the maximum bucket is <b>${buckets[buckets.length - 1]}</b>.
<br /><br />
The mean of the recorded data is <b>${formatNumber(mean)}</b>.
`;
}
// Clear chart for re-drawing,
// here we need to re-create the whole canvas
// otherwise we keep rebuilding the new graph on top of the previous
// and that causes hover madness
const canvas = document.createElement("canvas");
chartSpace.innerHTML = "";
chartSpace.appendChild(canvas);
// Draw the chart
const ctx = canvas.getContext("2d");
new Chart(ctx, {
type: "bar",
data: {
labels: buckets,
datasets: [{
barPercentage: .95,
categoryPercentage: 1,
backgroundColor: "rgba(76, 138, 196, 1)",
hoverBackgroundColor: "rgba(0, 89, 171, 1)",
data: percentages
}],
},
options: {
responsive: true,
scales: {
yAxes: [{
ticks: {
beginAtZero: true,
callback: value => `${value}%`
},
scaleLabel: {
display: true,
labelString: "Percentages of samples"
}
}],
xAxes: [{
ticks: {
autoSkip: false,
minRotation: 50,
maxRotation: 50,
beginAtZero: true,
callback: (value, index, values) => {
const interval = Math.floor(values.length / 25)
if (interval > 0 && index % interval != 0) {
return ""
} else {
return value
}
}
},
scaleLabel: {
display: true,
labelString: "Buckets"
}
}]
},
tooltips: {
mode: "index",
callbacks: {
title: () => null,
label: item => {
const index = item.index
const lastIndex = percentages.length - 1
const percentage = percentages[index].toFixed(2)
const value = formatNumber(data[index])
if (kind == "functional") {
return index == lastIndex ? `${value} samples (${percentage}%) where sample value > ${buckets[lastIndex]} (overflow)`
: `${value} samples (${percentage}%) where ${buckets[index]} ≤ sample value < ${buckets[index + 1]}`
} else {
return index == 0 ? `${value} samples (${percentage}%) where sample value < ${buckets[0]} (underflow)`
: index == lastIndex ? `${value} samples (${percentage}%) where sample value > ${buckets[lastIndex]} (overflow)`
: `${value} samples (${percentage}%) where ${buckets[index]} ≤ sample value < ${buckets[index + 1]}`
}
},
}
}
}
});
}
/**
* Build the data to be rendered in the charts.
*
* @param {String} kind The kind of histogram that should be build, possible values are "functional", "exponential" or "linear"
* @param {Object} props The properties related to the given histogram, keys differ based in the kind
* @param {String} dataOption The chosen way to build data, possible values are "normally-distributed", "log-normally-distributed", "uniformly-distributed" or "custom"
* @param {String} customData In case `dataOption` is "custom", this should contain a String containing a JSON array of numbers
* @param {function} transformation Option function to be applied to generated values
*
* @returns {Object} An object containing the bucket and values of a histogram
*/
function buildData (kind, props, dataOption, customData, transformation) {
if (kind == "functional") {
return buildDataFunctional(props, dataOption, customData, transformation);
} else {
return buildDataPreComputed(kind, props, dataOption, customData, transformation);
}
}
/**
* Build sample data or parse custom data.
*
* @param {String} dataOption The chosen way to build data, possible values are "normally-distributed", "log-normally-distributed", "uniformly-distributed" or "custom"
* @param {String} customData In case `dataOption` is "custom", this should contain a String containing a JSON array of numbers
* @param {Number} lower The lowest number the generated values may be, defaults to `1`
* @param {Number} upper The highest number the generated values may be, defaults to `100`
*
* @returns {Array} An array of values, this array has DATA_SAMPLE_COUNT length if not custom
*/
function buildSampleData (dataOption, customData, lower, upper) {
if (!lower) lower = 1;
if (!upper) upper = 100;
const values =
dataOption == "normally-distributed" ? normalRandomValues((lower + upper) / 2, (upper - lower) / 8, DATA_SAMPLE_COUNT)
: dataOption == "log-normally-distributed" ? logNormalRandomValues(Math.sqrt(Math.max(lower, 1) * upper), Math.pow(upper / Math.max(lower, 1), 1 / 8), DATA_SAMPLE_COUNT)
: dataOption == "uniformly-distributed" ? uniformValues(lower, upper, DATA_SAMPLE_COUNT)
: parseJSONString(customData);
return values;
}
/**
* Build the data to be rendered in the charts, in case histogram kind is "exponential" or "linear".
*
* @param {String} kind The kind of histogram that should be build, possible values are "functional", "exponential" or "linear"
* @param {Object} props The properties related to the given histogram, keys differ based in the kind
* @param {String} dataOption The chosen way to build data, possible values are "normally-distributed", "log-normally-distributed", "uniformly-distributed" or "custom"
* @param {String} customData In case `dataOption` is "custom", this should contain a String containing a JSON array of numbers
* @param {function} transformation Option function to be applied to generated values
*
* @returns {Object} An object containing the bucket and values of a histogram
*/
function buildDataPreComputed (kind, props, dataOption, customData, transformation) {
const { lowerBound, upperBound, bucketCount } = props;
const buckets = kind == "exponential"
? exponentialRange(lowerBound, upperBound, bucketCount)
: linearRange(lowerBound, upperBound, bucketCount);
const lowerBucket = buckets[0];
const upperBucket = buckets[buckets.length - 1];
const values = buildSampleData(dataOption, customData, lowerBucket, upperBucket)
.map(v => transformation && transformation(v));
const data = accumulateValuesIntoBucketsPreComputed(buckets, values)
return {
data,
buckets,
percentages: data.map(v => v * 100 / values.length),
};
}
/**
* Build the data to be rendered in the charts, in case histogram kind is "functional".
*
* @param {String} kind The kind of histogram that should be build, possible values are "functional", "exponential" or "linear"
* @param {Object} props The properties related to the given histogram, keys differ based in the kind
* @param {String} dataOption The chosen way to build data, possible values are "normally-distributed", "log-normally-distributed", "uniformly-distributed" or "custom"
* @param {String} customData In case `dataOption` is "custom", this should contain a String containing a JSON array of numbers
* @param {function} transformation Option function to be applied to generated values
*
* @returns {Object} An object containing the bucket and values of a histogram
*/
function buildDataFunctional(props, dataOption, customData, transformation) {
const { logBase, bucketsPerMagnitude, maximumValue } = props;
const values = buildSampleData(dataOption, customData)
.map(v => transformation && transformation(v));
const acc = accumulateValuesIntoBucketsFunctional(logBase, bucketsPerMagnitude, maximumValue, values);
const data = Object.values(acc)
return {
data,
buckets: Object.keys(acc),
percentages: data.map(v => v * 100 / values.length),
mean: values.reduce((sum, current) => sum + current) / values.length
};
}
/**
* Get the search params of the current URL.
*
* @returns {URLSearchParams} The search params object related to the current URL
*/
function searchParams() {
return (new URL(document.location)).searchParams;
}
/**
* Add a new param to the current pages URL, no relaoding
*
* @param {String} name The name of the param to set
* @param {String} value The value of the param to set
*/
function setURLSearchParam(name, value) {
let params = searchParams();
params.set(name, value);
history.pushState(null, null, `?${params.toString()}`);
}
/**
* Attempts to get a search param in the current pages URL with the same name as a given input,
* if such a param exists, set the value of the given input to the same value as the param found.
*
* @param {HTMLElement} input The input to update
*/
function setInputValueFromSearchParam(input) {
let param = searchParams().get(input.name);
if (param) input.value = param;
}
/**
* Finds the widest bucket in a list of buckets.
*
* The width of a bucket is defined by it's minimum value minus the previous buckets minimum value.
*
* @param {Array} buckets An array of buckets
*
* @returns {Number} The length of the widest bucket found
*/
function getWidestBucketWidth (buckets) {
let widest = 0;
for (let i = 1; i < buckets.length; i++) {
const currentWidth = buckets[i] - buckets[i - 1];
if (currentWidth > widest) {
widest = currentWidth;
}
}
return widest;
}
/**
* Attemps to parse a string as JSON, if unsuccesfull returns an empty array.
*
* @param {String} data A string containing a JSON encoded array
*
* @returns {Array} The parsed array
*/
function parseJSONString (data) {
let result = [];
try {
result = JSON.parse(data);
} finally {
return result;
}
}
/**
* Fills up a given textarea with dummy data.
*
* @param {HTMLElement} textarea The textarea to fill up
*/
function fillUpTextareaWithDummyData (textarea) {
const lower = 1;
const upper = 100;
const dummyData = logNormalRandomValues(Math.sqrt(Math.max(lower, 1) * upper), Math.pow(upper / Math.max(lower, 1), 1 / 8), DATA_SAMPLE_COUNT);
const prettyDummyData = JSON.stringify(dummyData, undefined, 4);
textarea.value = prettyDummyData;
}
/**
* Precomputes the buckets for an exponential histogram.
*
* This is copied and adapted from glean-core/src/histograms/exponential.rs
*
* @param {Number} min The minimum value that can be recorded on this histogram
* @param {Number} max The maximum value that can be recorded on this histogram
* @param {Number} bucketCount The number of buckets on this histogram
*
* @return {Array} The array of calculated buckets
*/
function exponentialRange (min, max, bucketCount) {
let logMax = Math.log(max);
let ranges = [0];
let current = min;
if (current == 0) {
current = 1;
}
ranges.push(current);
for (let i = 2; i < bucketCount; i++) {
let logCurrent = Math.log(current);
let logRatio = (logMax - logCurrent) / (bucketCount - i);
let logNext = logCurrent + logRatio;
let nextValue = Math.round(Math.exp(logNext));
current = nextValue > current ? nextValue : current + 1;
ranges.push(current);
}
return ranges;
}
/**
* Precomputes the buckets for an exponential histogram.
*
* This is copied and adapted from glean-core/src/histograms/linear.rs
*
* @param {Number} min The minimum value that can be recorded on this histogram
* @param {Number} max The maximum value that can be recorded on this histogram
* @param {Number} bucketCount The number of buckets on this histogram
*
* @return {Array} The array of calculated buckets
*/
function linearRange (min, max, bucketCount) {
let ranges = [0];
min = Math.max(1, min);
for (let i = 1; i < bucketCount; i++) {
let range = Math.round((min * (bucketCount - 1 - i) + max * (i - 1)) / (bucketCount - 2));
ranges.push(range);
}
return ranges;
}
/**
* Accumulate an array of values into buckets for histograms with pre-computed buckets
*
* @param {Array} buckets An array of buckets for a given histogram
* @param {Array} values The values to be recorded on this histogram
*
* @return {Array} The array of recorded values
*/
function accumulateValuesIntoBucketsPreComputed (buckets, values) {
let result = new Array(buckets.length).fill(0);
for (const value of values) {
let placed = false;
for (let i = 0; i < buckets.length - 1; i++) {
if (buckets[i] <= value && value < buckets[i + 1]) {
placed = true;
result[i]++;
break;
}
}
// If the value was not placed it is after the buckets limit,
// thus it fits in the last bucket
if (!placed) {
result[result.length - 1]++;
}
}
return result;
}
/**
* Accumulate an array of values into buckets for histograms with dinamically created buckets.
*
* For these types of histograms bucketing is performed by a function, rather than pre-computed buckets.
* The bucket index of a given sample is determined with the following function:
*
* i = n log<sub>base</sub>(𝑥)
*
* In other words, there are n buckets for each power of `base` magnitude.
*
* Based on glean-core/src/histograms/functional.rs
*
* @param {Number} logBase The log base for the bucketing algorithm
* @param {Array} bucketsPerMagnitude How many buckets to create per magnitude
* @param {Number} maximumValue The maximum value that can be recorded on this histogram
* @param {Array} values The values to be recorded on this histogram
*
* @return {Object} An object mapping buckets and recorded values of this histogram
*/
function accumulateValuesIntoBucketsFunctional (logBase, bucketsPerMagnitude, maximumValue, values) {
const exponent = Math.pow(logBase, 1 / bucketsPerMagnitude);
const sampleToBucketIndex = sample => Math.floor(log(sample + 1, exponent));;
const bucketIndexToBucketMinimum = index => Math.floor(Math.pow(exponent, index));
const sampleToBucketMinimum = sample => {
let bucketMinimum;
if (sample == 0) {
bucketMinimum = 0;
} else {
const bucketIndex = sampleToBucketIndex(sample);
bucketMinimum = bucketIndexToBucketMinimum(bucketIndex);
}
return bucketMinimum;
}
let result = {};
let min, max;
for (let value of values) {
// Cap on the maximum value
if (value > maximumValue) {
value = maximumValue;
}
const bucketMinimum = String(sampleToBucketMinimum(value));
if (!(bucketMinimum in result)) {
result[bucketMinimum] = 0;
}
result[bucketMinimum]++;
// Keep track of the max and min values accumulated,
// we will need them later
if (!min || value < min) min = value;
if (!max || value > max) max = value;
}
// Fill in missing buckets,
// this is based on the snapshot() function
const minBucket = sampleToBucketIndex(min);
const maxBucket = sampleToBucketIndex(max) + 1;
for (let idx = minBucket; idx <= maxBucket; idx++) {
let bucketMinimum = String(bucketIndexToBucketMinimum(idx));
if (!(bucketMinimum in result)) {
result[bucketMinimum] = 0;
}
}
return result;
}
/**
* Box-Muller transform in polar form.
*
* Values below zero will be truncated to 0.
*
* Copied over and adapted
* from https://github.com/mozilla/telemetry-dashboard/blob/bd7c213391d4118553b9ff1791ed0441bf912c60/histogram-simulator/simulator.js
*
* @param {Number} mu
* @param {Number} sigma
* @param {Number} count The length of the generated array
*
* @return {Array} An array of generated values
*/
function normalRandomValues (mu, sigma, count) {
let values = [];
let z0, z1, value;
for (let i = 0; values.length < count; i++) {
if (i % 2 === 0) {
let x1, x2, w;
do {
x1 = 2 * Math.random() - 1;
x2 = 2 * Math.random() - 1;
w = x1 * x1 + x2 * x2;
} while (w >= 1)
w = Math.sqrt((-2 * Math.log(w)) / w);
z0 = x1 * w;
z1 = x2 * w;
value = z0;
} else {
value = z1;
}
value = value * sigma + mu;
values.push(value);
}
return values.map(value => value >= 0 ? Math.floor(value) : 0);
}
/**
* Box-Muller transform in polar form for log-normal distributions
*
* Values below zero will be truncated to 0.
*
* Copied over and adapted
* from https://github.com/mozilla/telemetry-dashboard/blob/bd7c213391d4118553b9ff1791ed0441bf912c60/histogram-simulator/simulator.js
*
* @param {Number} mu
* @param {Number} sigma
* @param {Number} count The length of the generated array
*
* @return {Array} An array of generated values
*/
function logNormalRandomValues (mu, sigma, count) {
let values = [];
let z0, z1, value;
for (let i = 0; i < count; i++) {
if (i % 2 === 0) {
let x1, x2, w;
do {
x1 = 2 * Math.random() - 1;
x2 = 2 * Math.random() - 1;
w = x1 * x1 + x2 * x2;
} while (w >= 1)
w = Math.sqrt((-2 * Math.log(w)) / w);
z0 = x1 * w;
z1 = x2 * w;
value = z0;
} else {
value = z1;
}
value = Math.exp(value * Math.log(sigma) + Math.log(mu));
values.push(value);
}
return values.map(value => value >= 0 ? Math.floor(value) : 0);
}
/**
* A uniformly distributed array of random values
*
* @param {Number} min The minimum value this function may generate
* @param {Number} max The maximum value this function may generate
* @param {Number} count The length of the generated array
*
* @return {Array} An array of generated values
*/
function uniformValues (min, max, count) {
let values = [];
for (var i = 0; i <= count; i++) {
values.push(Math.random() * (max - min) + min);
}
return values;
}
/**
* Formats a number as a string.
*
* Copied over and adapted
* from https://github.com/mozilla/telemetry-dashboard/blob/bd7c213391d4118553b9ff1791ed0441bf912c60/histogram-simulator/simulator.js
*
* @param {Number} number The number to format
*
* @return {String} The formatted number
*/
function formatNumber(number) {
if (number == Infinity) return "Infinity";
if (number == -Infinity) return "-Infinity";
if (isNaN(number)) return "NaN";
const mag = Math.abs(number);
const exponent =
Math.log10 !== undefined ? Math.floor(Math.log10(mag))
: Math.floor(Math.log(mag) / Math.log(10));
const interval = Math.pow(10, Math.floor(exponent / 3) * 3);
const units = {
1000: "k",
1000000: "M",
1000000000: "B",
1000000000000: "T"
};
if (interval in units) {
return Math.round(number * 100 / interval) / 100 + units[interval];
}
return Math.round(number * 100) / 100;
}
/**
* Arbitrary base log function, Javascript doesn't have one
*
* @param {Number} number A numeric expression
* @param {base} base The log base
*
* @return {Number} The calculation result
*/
function log(number, base) {
return Math.log(number) / Math.log(base);
}

7
docs/chart.min.js поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -26,7 +26,6 @@
background-color: #ccc;
}
/* The container that holds all of the tab contents */
.tabcontents {
display: flex;
@ -47,3 +46,119 @@ footer#open-on-gh {
border-top: 1px solid black;
padding: 5px 0;
}
/* Distribution simulator styles */
#simulator-container {
display: flex;
flex-wrap: wrap;
overflow: hidden;
}
#simulator-container h3 {
margin-top: 20px;
}
#simulator-container .input-group {
width: 100%;
}
#simulator-container .input-group label {
display: block;
font-weight: bold;
font-size: 14px;
margin-bottom: 7px;
}
#simulator-container .input-group input,
#simulator-container .input-group select,
#custom-data-modal textarea {
display: block;
width: 100%;
padding: 5px;
margin-bottom: 10px;
border-radius: 3px;
border: 1px solid #e0e0e0;
box-sizing: border-box;
}
#histogram-props,
#data-options {
width: 50%;
box-sizing: border-box;
}
#data-options {
padding-right: 50px;
}
#data-options .input-group {
margin-bottom: 10px;
}
#data-options .input-group:first-of-type {
margin-top: 20px;
}
#data-options .input-group:last-of-type {
margin-bottom: 0;
}
#data-options .input-group label {
display: inline-block;
}
#data-options .input-group input {
display: inline;
width: auto;
}
#histogram-chart-container {
width: 100%;
padding: 30px;
border: 1px solid #e0e0e0;
margin: 30px 0;
overflow: hidden;
position: relative;
}
#histogram-chart {
margin-top: 50px;
width: 100%;
}
#histogram-chart-legend {
font-size: 14px;
text-align: center;
width: 100%;
}
#histogram-functional-props,
#histogram-non-functional-props {
display: none;
}
#custom-data-modal-overlay {
background-color: rgba(0, 0, 0, .5);
position: fixed;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
z-index: 999;
display: none;
}
#custom-data-modal {
width: 50%;
background-color: white;
border-radius: 5px;
position: relative;
top: 15%;
left: 25%;
padding: 50px;
}
.hide {
display: none !important;
}

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

@ -21,6 +21,8 @@ Custom distributions have the following required parameters:
- `linear`: The buckets are evenly spaced
- `exponential`: The buckets follow a natural logarithmic distribution
> **Note** Check out how these bucketing algorithms would behave on the [Custom distribution simulator](#simulator)
In addition, the metric should specify:
- `unit`: (String) The unit of the values in the metric. For documentation purposes only -- does not affect data collection.
@ -86,3 +88,60 @@ assertEquals(1, Graphics.checkerboardPeak.testGetNumRecordedErrors(ErrorType.Inv
## Reference
* [Kotlin API docs](../../../javadoc/glean/mozilla.telemetry.glean.private/-custom-distribution-metric-type/index.html)
## Simulator
<div id="custom-data-modal-overlay">
<div id="custom-data-modal">
<p>Please, insert your custom data below as a JSON array.</p>
<textarea rows="30"></textarea>
</div>
</div>
<div id="simulator-container">
<div id="histogram-chart-container">
<div id="histogram-chart"></div>
<p id="histogram-chart-legend"><p>
</div>
<div id="data-options">
<h3>Data options</h3>
<div class="input-group">
<label for="normally-distributed">Generate normally distributed data</label>
<input name="data-options" value="normally-distributed" id="normally-distributed" type="radio" />
</div>
<div class="input-group">
<label for="log-normally-distributed">Generate log-normally distributed data</label>
<input name="data-options" value="log-normally-distributed" id="log-normally-distributed" type="radio" checked />
</div>
<div class="input-group">
<label for="uniformly-distributed">Generate uniformly distributed data</label>
<input name="data-options" value="uniformly-distributed" id="uniformly-distributed" type="radio" />
</div>
<div class="input-group" id="custom-data-input-group">
<label for="custom">Use custom data</label>
<input name="data-options" value="custom" id="custom" type="radio" />
</div>
</div>
<div id="histogram-props">
<h3>Properties</h3>
<div class="input-group">
<label for="kind">Histogram type (<code>histogram_type</code>)</label>
<select id="kind" name="kind">
<option value="exponential" selected>Exponential</option>
<option value="linear">Linear</option>
</select>
</div>
<div class="input-group">
<label for="lower-bound">Range minimum (<code>range_min</code>)</label>
<input name="lower-bound" id="lower-bound" type="number" value="1" />
</div>
<div class="input-group">
<label for="upper-bound">Range maximum (<code>range_max</code>)</label>
<input name="upper-bound" id="upper-bound" type="number" value="500" />
</div>
<div class="input-group">
<label for="bucket-count">Bucket count (<code>bucket_count</code>)</label>
<input name="bucket-count" id="bucket-count" type="number" value="20" />
</div>
</div>
</div>

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

@ -9,6 +9,8 @@ That is, the function from a value \\( x \\) to a bucket index is:
This makes them suitable for measuring memory sizes on a number of different scales without any configuration.
> **Note** Check out how this bucketing algorithm would behave on the [Simulator](#simulator)
## Configuration
If you wanted to create a memory distribution to measure the amount of heap memory allocated, first you need to add an entry for it to the `metrics.yaml` file:
@ -193,3 +195,71 @@ Assert.Equal(1, Memory.heapAllocated.TestGetNumRecordedErrors(ErrorType.InvalidV
* [Kotlin API docs](../../../javadoc/glean/mozilla.telemetry.glean.private/-memory-distribution-metric-type/index.html)
* [Swift API docs](../../../swift/Classes/MemoryDistributionMetricType.html)
* [Python API docs](../../../python/glean/metrics/timing_distribution.html)
## Simulator
<div id="custom-data-modal-overlay">
<div id="custom-data-modal">
<p>Please, insert your custom data below as a JSON array.</p>
<textarea rows="30"></textarea>
</div>
</div>
<div id="simulator-container">
<div id="histogram-chart-container">
<div id="histogram-chart"></div>
<p id="histogram-chart-legend"><p>
</div>
<div id="data-options">
<h3>Data options</h3>
<div class="input-group">
<label for="normally-distributed">Generate normally distributed data</label>
<input name="data-options" value="normally-distributed" id="normally-distributed" type="radio" />
</div>
<div class="input-group">
<label for="log-normally-distributed">Generate log-normally distributed data</label>
<input name="data-options" value="log-normally-distributed" id="log-normally-distributed" type="radio" checked />
</div>
<div class="input-group">
<label for="uniformly-distributed">Generate uniformly distributed data</label>
<input name="data-options" value="uniformly-distributed" id="uniformly-distributed" type="radio" />
</div>
<div class="input-group" id="custom-data-input-group">
<label for="custom">Use custom data</label>
<input name="data-options" value="custom" id="custom" type="radio" />
</div>
</div>
<div id="histogram-props">
<h3>Properties</h3>
<div class="input-group hide">
<label for="kind">Histogram type</label>
<select id="kind" name="kind" disabled>
<option value="functional" selected>Functional</option>
</select>
</div>
<div class="input-group hide">
<label for="log-base">Log base</label>
<input id="log-base" name="log-base" type="number" value="2" disabled />
</div>
<div class="input-group hide">
<label for="buckets-per-magnitude">Buckets per magnitude</label>
<input id="buckets-per-magnitude" name="buckets-per-magnitude" type="number" value="16" disabled />
</div>
<div class="input-group hide">
<label for="maximum-value">Maximum value</label>
<input id="maximum-value" name="maximum-value" type="number" value="1099511627776" disabled />
</div>
<div class="input-group">
<label for="memory-unit">Memory unit (<code>memory_unit</code>)</label>
<select id="memory-unit" name="memory-unit">
<option value="byte" selected>Byte</option>
<option value="kilobyte">Kilobyte</option>
<option value="megabyte">Megabyte</option>
<option value="gigabyte">Gigabyte</option>
</select>
</div>
</div>
</div>
> **Note** The data _provided_, is assumed to be in the configured memory unit. The data _recorded_, on the other hand, is always in **bytes**.
> This means that, if the configured memory unit is not `byte`, the data will be transformed before being recorded. Notice this, by using the select field above to change the memory unit and see the mean of the data recorded changing.

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

@ -11,6 +11,8 @@ That is, the function from a value \\( x \\) to a bucket index is:
This makes them suitable for measuring timings on a number of time scales without any configuration.
> **Note** Check out how this bucketing algorithm would behave on the [Simulator](#simulator)
Timings always span the full length between `start` and `stopAndAccumulate`.
If the Glean upload is disabled when calling `start`, the timer is still started.
If the Glean upload is disabled at the time `stopAndAccumulate` is called, nothing is recorded.
@ -310,3 +312,71 @@ Assert.Equal(1, Pages.pageLoad.TestGetNumRecordedErrors(ErrorType.InvalidValue))
* [Kotlin API docs](../../../javadoc/glean/mozilla.telemetry.glean.private/-timing-distribution-metric-type/index.html)
* [Swift API docs](../../../swift/Classes/TimingDistributionMetricType.html)
* [Python API docs](../../../python/glean/metrics/timing_distribution.html)
## Simulator
<div id="custom-data-modal-overlay">
<div id="custom-data-modal">
<p>Please, insert your custom data below as a JSON array.</p>
<textarea rows="30"></textarea>
</div>
</div>
<div id="simulator-container">
<div id="histogram-chart-container">
<div id="histogram-chart"></div>
<p id="histogram-chart-legend"><p>
</div>
<div id="data-options">
<h3>Data options</h3>
<div class="input-group">
<label for="normally-distributed">Generate normally distributed data</label>
<input name="data-options" value="normally-distributed" id="normally-distributed" type="radio" />
</div>
<div class="input-group">
<label for="log-normally-distributed">Generate log-normally distributed data</label>
<input name="data-options" value="log-normally-distributed" id="log-normally-distributed" type="radio" checked />
</div>
<div class="input-group">
<label for="uniformly-distributed">Generate uniformly distributed data</label>
<input name="data-options" value="uniformly-distributed" id="uniformly-distributed" type="radio" />
</div>
<div class="input-group" id="custom-data-input-group">
<label for="custom">Use custom data</label>
<input name="data-options" value="custom" id="custom" type="radio" />
</div>
</div>
<div id="histogram-props">
<h3>Properties</h3>
<div class="input-group hide">
<label for="kind">Histogram type</label>
<select id="kind" name="kind" disabled>
<option value="functional" selected>Functional</option>
</select>
</div>
<div class="input-group hide">
<label for="log-base">Log base</label>
<input id="log-base" name="log-base" type="number" value="2" disabled />
</div>
<div class="input-group hide">
<label for="buckets-per-magnitude">Buckets per magnitude</label>
<input id="buckets-per-magnitude" name="buckets-per-magnitude" type="number" value="8" disabled />
</div>
<div class="input-group hide">
<label for="maximum-value">Maximum value</label>
<input id="maximum-value" name="maximum-value" type="number" value="600000000000" disabled />
</div>
<div class="input-group">
<label for="time-unit">Time unit (<code>time_unit</code>)</label>
<select id="time-unit" name="time-unit">
<option value="nanoseconds" selected>Nanoseconds</option>
<option value="microseconds">Microseconds</option>
<option value="milliseconds">Milliseconds</option>
</select>
</div>
</div>
</div>
> **Note** The data _provided_, is assumed to be in the configured time unit. The data _recorded_, on the other hand, is always in **nanoseconds**.
> This means that, if the configured time unit is not `nanoseconds`, the data will be transformed before being recorded. Notice this, by using the select field above to change the time unit and see the mean of the data recorded changing.