blob: f2223b841f45a6f7608a1f7f6ce49701ca148103 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright 2016 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<link rel="import" href="/tracing/base/utils.html">
<link rel="import" href="/tracing/value/histogram.html">
<link rel="import" href="/tracing/value/histogram_deserializer.html">
<link rel="import" href="/tracing/value/histogram_grouping.html">
<script>
'use strict';
tr.exportTo('tr.v', function() {
class HistogramSet {
constructor(opt_histograms) {
this.histograms_ = new Set();
this.sharedDiagnosticsByGuid_ = new Map();
if (opt_histograms !== undefined) {
for (const hist of opt_histograms) {
this.addHistogram(hist);
}
}
}
has(hist) {
return this.histograms_.has(hist);
}
/**
* Create a Histogram, configure it, add samples to it, and add it to this
* HistogramSet.
*
* |samples| can be either
* 0. a number, or
* 1. a dictionary {value: number, diagnostics: dictionary}, or
* 2. an array of
* 2a. number, or
* 2b. dictionaries {value, diagnostics}.
*
* @param {string} name
* @param {!tr.b.Unit} unit
* @param {number|!Object|!Array.<(number|!Object)>} samples
* @param {!Object=} opt_options
* @param {!tr.v.HistogramBinBoundaries} opt_options.binBoundaries
* @param {!Object|!Map} opt_options.summaryOptions
* @param {!Object|!Map} opt_options.diagnostics
* @param {string} opt_options.description
* @return {!tr.v.Histogram}
*/
createHistogram(name, unit, samples, opt_options) {
const hist = tr.v.Histogram.create(name, unit, samples, opt_options);
this.addHistogram(hist);
return hist;
}
/**
* @param {!tr.v.Histogram} hist
* @param {(!Object|!tr.v.d.DiagnosticMap)=} opt_diagnostics
*/
addHistogram(hist, opt_diagnostics) {
if (this.has(hist)) {
throw new Error('Cannot add same Histogram twice');
}
if (opt_diagnostics !== undefined) {
if (!(opt_diagnostics instanceof Map)) {
opt_diagnostics = Object.entries(opt_diagnostics);
}
for (const [name, diagnostic] of opt_diagnostics) {
hist.diagnostics.set(name, diagnostic);
}
}
this.histograms_.add(hist);
}
/**
* Add a Diagnostic to all Histograms so that it will only be serialized
* once per HistogramSet rather than once per Histogram that contains it.
*
* @param {string} name
* @param {!tr.v.d.Diagnostic} diagnostic
*/
addSharedDiagnosticToAllHistograms(name, diagnostic) {
this.addSharedDiagnostic(diagnostic);
for (const hist of this) {
hist.diagnostics.set(name, diagnostic);
}
}
/**
* Add a Diagnostic to this HistogramSet so that it will only be serialized
* once per HistogramSet rather than once per Histogram that contains it.
*
* @param {!tr.v.d.Diagnostic} diagnostic
*/
addSharedDiagnostic(diagnostic) {
this.sharedDiagnosticsByGuid_.set(diagnostic.guid, diagnostic);
}
get length() {
return this.histograms_.size;
}
* [Symbol.iterator]() {
for (const hist of this.histograms_) {
yield hist;
}
}
/**
* Filters Histograms by matching their name exactly.
*
* @param {string} name Histogram name.
* @return {!Array.<!tr.v.Histogram>}
*/
getHistogramsNamed(name) {
return [...this].filter(h => h.name === name);
}
/**
* Filters to find the Histogram that matches the specified name exactly.
* If no Histogram with that name exists, undefined is returned. If multiple
* Histograms with the name exist, an error is thrown.
*
* @param {string} name Histogram name.
* @return {tr.v.Histogram}
*/
getHistogramNamed(name) {
const histograms = this.getHistogramsNamed(name);
if (histograms.length === 0) return undefined;
if (histograms.length > 1) {
throw new Error(
`Unexpectedly found multiple histograms named "${name}"`);
}
return histograms[0];
}
/**
* Lookup a Diagnostic by its guid.
*
* @param {string} guid
* @return {!tr.v.d.Diagnostic|undefined}
*/
lookupDiagnostic(guid) {
return this.sharedDiagnosticsByGuid_.get(guid);
}
deserialize(data) {
for (const hist of tr.v.HistogramDeserializer.deserialize(data)) {
this.addHistogram(hist);
}
}
/**
* Convert dicts to either Histograms or shared Diagnostics.
*
* @param {!Object} dicts
*/
importDicts(dicts) {
// The new HistogramSet JSON format is an array of at least 3 arrays.
if ((dicts instanceof Array) &&
(dicts.length > 2) &&
(dicts[0] instanceof Array)) {
this.deserialize(dicts);
return;
}
// The original HistogramSet JSON format was a flat array of objects.
for (const dict of dicts) {
this.importLegacyDict(dict);
}
}
/**
* Convert dict to either a Histogram or a shared Diagnostic.
*
* @param {!Object} dict
*/
importLegacyDict(dict) {
if (dict.type !== undefined) {
// TODO(benjhayden): Forget about TagMaps in 2019Q2.
if (dict.type === 'TagMap') return;
if (!tr.v.d.Diagnostic.findTypeInfoWithName(dict.type)) {
throw new Error('Unrecognized shared diagnostic type ' + dict.type);
}
this.sharedDiagnosticsByGuid_.set(dict.guid,
tr.v.d.Diagnostic.fromDict(dict));
} else {
const hist = tr.v.Histogram.fromDict(dict);
this.addHistogram(hist);
hist.diagnostics.resolveSharedDiagnostics(this, true);
}
}
/**
* Serialize all of the Histograms and shared Diagnostics to an Array of
* dictionaries.
*
* @return {!Array.<!Object>}
*/
asDicts() {
const dicts = [];
for (const diagnostic of this.sharedDiagnosticsByGuid_.values()) {
dicts.push(diagnostic.asDict());
}
for (const hist of this) {
dicts.push(hist.asDict());
}
return dicts;
}
/**
* Find the Histograms whose names are not contained in any other
* Histograms' RelatedNameMap diagnostics.
*
* @return {!Array.<!tr.v.Histogram>}
*/
get sourceHistograms() {
const diagnosticNames = new Set();
for (const hist of this) {
for (const diagnostic of hist.diagnostics.values()) {
if (!(diagnostic instanceof tr.v.d.RelatedNameMap)) continue;
for (const name of diagnostic.values()) {
diagnosticNames.add(name);
}
}
}
const sourceHistograms = new HistogramSet;
for (const hist of this) {
if (!diagnosticNames.has(hist.name)) {
sourceHistograms.addHistogram(hist);
}
}
return sourceHistograms;
}
/**
* Return a nested Map, whose keys are strings and leaf values are Arrays of
* Histograms.
* See GROUPINGS for example |groupings|.
* Groupings are skipped when |opt_skipGroupingCallback| is specified and
* returns true.
*
* @typedef {!Array.<tr.v.Histogram>} HistogramArray
* @typedef {!Map.<string,!(HistogramArray|HistogramArrayMap)>}
* HistogramArrayMap
* @typedef {!Map.<string,!HistogramArray>} LeafHistogramArrayMap
*
* @param {!Array.<!tr.v.HistogramGrouping>} groupings
* @param {!function(!Grouping, !LeafHistogramArrayMap):boolean=}
* opt_skipGroupingCallback
*
* @return {!(HistogramArray|HistogramArrayMap)}
*/
groupHistogramsRecursively(groupings, opt_skipGroupingCallback) {
function recurse(histograms, level) {
if (level === groupings.length) {
return histograms; // recursion base case
}
const grouping = groupings[level];
const groupedHistograms = tr.b.groupIntoMap(
histograms, grouping.callback);
if (opt_skipGroupingCallback && opt_skipGroupingCallback(
grouping, groupedHistograms)) {
return recurse(histograms, level + 1);
}
for (const [key, group] of groupedHistograms) {
groupedHistograms.set(key, recurse(group, level + 1));
}
return groupedHistograms;
}
return recurse([...this], 0);
}
/*
* Histograms and Diagnostics are merged two at a time, without considering
* any others, so it is possible for two merged Diagnostics to be equivalent
* but not identical, which is inefficient. This method replaces equivalent
* Diagnostics with shared Diagnostics so that the HistogramSet can be
* serialized more efficiently and so that these Diagnostics can be compared
* quickly when merging relationship Diagnostics.
*/
deduplicateDiagnostics() {
const namesToCandidates = new Map(); // string: Set<Diagnostic>
const diagnosticsToHistograms = new Map(); // Diagnostic: [Histogram]
const keysToDiagnostics = new Map(); // string: Diagnostic
for (const hist of this) {
for (const [name, candidate] of hist.diagnostics) {
// TODO(#3695): Remove this check once equality is smoke-tested.
if (candidate.equals === undefined) {
this.sharedDiagnosticsByGuid_.set(candidate.guid, candidate);
continue;
}
const hashKey = candidate.hashKey;
if (candidate.hashKey !== undefined) {
// TODO(857283): Fall back to slow path if same name but diff type
if (keysToDiagnostics.has(hashKey)) {
hist.diagnostics.set(name, keysToDiagnostics.get(hashKey));
} else {
keysToDiagnostics.set(hashKey, candidate);
this.sharedDiagnosticsByGuid_.set(candidate.guid, candidate);
}
continue;
}
if (diagnosticsToHistograms.get(candidate) === undefined) {
diagnosticsToHistograms.set(candidate, [hist]);
} else {
diagnosticsToHistograms.get(candidate).push(hist);
}
if (!namesToCandidates.has(name)) {
namesToCandidates.set(name, new Set());
}
namesToCandidates.get(name).add(candidate);
}
}
for (const [name, candidates] of namesToCandidates) {
const deduplicatedDiagnostics = new Set();
for (const candidate of candidates) {
let found = false;
for (const test of deduplicatedDiagnostics) {
if (candidate.equals(test)) {
const hists = diagnosticsToHistograms.get(candidate);
for (const hist of hists) {
hist.diagnostics.set(name, test);
}
found = true;
break;
}
}
if (!found) {
deduplicatedDiagnostics.add(candidate);
}
for (const diagnostic of deduplicatedDiagnostics) {
this.sharedDiagnosticsByGuid_.set(diagnostic.guid, diagnostic);
}
}
}
}
/**
* @param {!Iterable.<string>} names of GenericSet diagnostics
* @return {!Array.<!tr.v.HistogramGrouping>}
*/
buildGroupingsFromTags(names) {
const tags = new Map(); // name: Set<string>
for (const hist of this) {
for (const name of names) {
if (!hist.diagnostics.has(name)) continue;
if (!tags.has(name)) tags.set(name, new Set());
for (const tag of hist.diagnostics.get(name)) {
tags.get(name).add(tag);
}
}
}
const groupings = [];
for (const [name, values] of tags) {
const built = tr.v.HistogramGrouping.buildFromTags(values, name);
for (const grouping of built) {
groupings.push(grouping);
}
}
return groupings;
}
}
return {HistogramSet};
});
</script>