Commit a175853c authored by Nikunj Bhagat's avatar Nikunj Bhagat Committed by Commit Bot

Add chrome://ukm debug ui

BUG=843181

Change-Id: I8b43a72535dd09a175df731a94dc8e216374e7ff
Reviewed-on: https://chromium-review.googlesource.com/c/1306660
Commit-Queue: Nik Bhagat <nikunjb@chromium.org>
Reviewed-by: default avatarJochen Eisinger <jochen@chromium.org>
Reviewed-by: default avatarcalamity <calamity@chromium.org>
Reviewed-by: default avatarSteven Holte <holte@chromium.org>
Reviewed-by: default avatarMikel Astiz <mastiz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#606660}
parent 3ec949fd
...@@ -1223,6 +1223,7 @@ if (closure_compile) { ...@@ -1223,6 +1223,7 @@ if (closure_compile) {
"chrome/browser/resources:closure_compile", "chrome/browser/resources:closure_compile",
"content/browser/resources:closure_compile", "content/browser/resources:closure_compile",
"ui/webui/resources:closure_compile", "ui/webui/resources:closure_compile",
"components/ukm/debug:closure_compile",
] ]
if (is_chromeos) { if (is_chromeos) {
data_deps += [ "ui/file_manager:closure_compile" ] data_deps += [ "ui/file_manager:closure_compile" ]
......
...@@ -69,8 +69,8 @@ public class UkmTest { ...@@ -69,8 +69,8 @@ public class UkmTest {
public boolean isUkmEnabled(Tab normalTab) throws Exception { public boolean isUkmEnabled(Tab normalTab) throws Exception {
String state = getElementContent(normalTab, "state"); String state = getElementContent(normalTab, "state");
Assert.assertTrue( Assert.assertTrue(
"UKM state: " + state, state.equals("\"True\"") || state.equals("\"False\"")); "UKM state: " + state, state.equals("\"ENABLED\"") || state.equals("\"DISABLED\""));
return state.equals("\"True\""); return state.equals("\"ENABLED\"");
} }
public String getUkmClientId(Tab normalTab) throws Exception { public String getUkmClientId(Tab normalTab) throws Exception {
......
...@@ -158,6 +158,7 @@ ...@@ -158,6 +158,7 @@
<include name="IDR_DOWNLOAD_INTERNALS_VISUALS_JS" file="resources\download_internals\download_internals_visuals.js" type="BINDATA" compress="gzip" /> <include name="IDR_DOWNLOAD_INTERNALS_VISUALS_JS" file="resources\download_internals\download_internals_visuals.js" type="BINDATA" compress="gzip" />
<include name="IDR_UKM_INTERNALS_HTML" file="../../components/ukm/debug/ukm_internals.html" flattenhtml="true" allowexternalscript="true" compress="gzip" type="BINDATA" /> <include name="IDR_UKM_INTERNALS_HTML" file="../../components/ukm/debug/ukm_internals.html" flattenhtml="true" allowexternalscript="true" compress="gzip" type="BINDATA" />
<include name="IDR_UKM_INTERNALS_JS" file="../../components/ukm/debug/ukm_internals.js" flattenhtml="true" compress="gzip" type="BINDATA" /> <include name="IDR_UKM_INTERNALS_JS" file="../../components/ukm/debug/ukm_internals.js" flattenhtml="true" compress="gzip" type="BINDATA" />
<include name="IDR_UKM_INTERNALS_CSS" file="../../components/ukm/debug/ukm_internals.css" flattenhtml="true" compress="gzip" type="BINDATA" />
<if expr="not is_android"> <if expr="not is_android">
<include name="IDR_MD_DOWNLOADS_1X_INCOGNITO_MARKER_PNG" file="resources\md_downloads\1x\incognito_marker.png" type="BINDATA" /> <include name="IDR_MD_DOWNLOADS_1X_INCOGNITO_MARKER_PNG" file="resources\md_downloads\1x\incognito_marker.png" type="BINDATA" />
<include name="IDR_MD_DOWNLOADS_2X_INCOGNITO_MARKER_PNG" file="resources\md_downloads\2x\incognito_marker.png" type="BINDATA" /> <include name="IDR_MD_DOWNLOADS_2X_INCOGNITO_MARKER_PNG" file="resources\md_downloads\2x\incognito_marker.png" type="BINDATA" />
......
...@@ -29,6 +29,7 @@ content::WebUIDataSource* CreateUkmHTMLSource() { ...@@ -29,6 +29,7 @@ content::WebUIDataSource* CreateUkmHTMLSource() {
content::WebUIDataSource::Create(chrome::kChromeUIUkmHost); content::WebUIDataSource::Create(chrome::kChromeUIUkmHost);
source->AddResourcePath("ukm_internals.js", IDR_UKM_INTERNALS_JS); source->AddResourcePath("ukm_internals.js", IDR_UKM_INTERNALS_JS);
source->AddResourcePath("ukm_internals.css", IDR_UKM_INTERNALS_CSS);
source->SetDefaultResource(IDR_UKM_INTERNALS_HTML); source->SetDefaultResource(IDR_UKM_INTERNALS_HTML);
source->UseGzip(); source->UseGzip();
return source; return source;
......
// Copyright 2018 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.
module.exports = {
'env': {'browser': true, 'es6': true,},
'rules': {
'no-var': 'error',
},
};
# Copyright 2018 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.
def _CommonChecks(input_api, output_api):
import web_dev_style.presubmit_support
return (
web_dev_style.presubmit_support.CheckStyleESLint(input_api, output_api) +
input_api.canned_checks.CheckPatchFormatted(
input_api, output_api, check_js=True))
def CheckChangeOnUpload(input_api, output_api):
return _CommonChecks(input_api, output_api)
def CheckChangeOnCommit(input_api, output_api):
return _CommonChecks(input_api, output_api)
...@@ -20,6 +20,8 @@ namespace debug { ...@@ -20,6 +20,8 @@ namespace debug {
namespace { namespace {
static const uint64_t BIT_FILTER_LAST32 = 0xffffffffULL;
struct SourceData { struct SourceData {
UkmSource* source; UkmSource* source;
std::vector<mojom::UkmEntry*> entries; std::vector<mojom::UkmEntry*> entries;
...@@ -38,8 +40,8 @@ base::Value ConvertEntryToValue(const ukm::builders::DecodeMap& decode_map, ...@@ -38,8 +40,8 @@ base::Value ConvertEntryToValue(const ukm::builders::DecodeMap& decode_map,
const auto it = decode_map.find(entry.event_hash); const auto it = decode_map.find(entry.event_hash);
if (it == decode_map.end()) { if (it == decode_map.end()) {
entry_value.SetKey("name", entry_value.SetKey(
base::Value(static_cast<double>(entry.event_hash))); "name", UkmDebugDataExtractor::UInt64AsPairOfInt(entry.event_hash));
} else { } else {
entry_value.SetKey("name", base::Value(it->second.name)); entry_value.SetKey("name", base::Value(it->second.name));
...@@ -49,8 +51,8 @@ base::Value ConvertEntryToValue(const ukm::builders::DecodeMap& decode_map, ...@@ -49,8 +51,8 @@ base::Value ConvertEntryToValue(const ukm::builders::DecodeMap& decode_map,
base::DictionaryValue metric_value; base::DictionaryValue metric_value;
metric_value.SetKey("name", metric_value.SetKey("name",
base::Value(GetName(it->second, metric.first))); base::Value(GetName(it->second, metric.first)));
metric_value.SetKey("value", metric_value.SetKey(
base::Value(static_cast<double>(metric.second))); "value", UkmDebugDataExtractor::UInt64AsPairOfInt(metric.second));
metrics_list_storage->push_back(std::move(metric_value)); metrics_list_storage->push_back(std::move(metric_value));
} }
entry_value.SetKey("metrics", std::move(metrics_list)); entry_value.SetKey("metrics", std::move(metrics_list));
...@@ -64,6 +66,17 @@ UkmDebugDataExtractor::UkmDebugDataExtractor() = default; ...@@ -64,6 +66,17 @@ UkmDebugDataExtractor::UkmDebugDataExtractor() = default;
UkmDebugDataExtractor::~UkmDebugDataExtractor() = default; UkmDebugDataExtractor::~UkmDebugDataExtractor() = default;
// static
base::Value UkmDebugDataExtractor::UInt64AsPairOfInt(uint64_t v) {
// Convert int64_t to pair of int. Passing int64_t in base::Value is not
// supported. The pair of int will be passed as a ListValue.
base::Value::ListStorage int_pair;
int_pair.push_back(
base::Value(static_cast<int>((v >> 32) & BIT_FILTER_LAST32)));
int_pair.push_back(base::Value(static_cast<int>(v & BIT_FILTER_LAST32)));
return base::Value(int_pair);
}
// static // static
base::Value UkmDebugDataExtractor::GetStructuredData( base::Value UkmDebugDataExtractor::GetStructuredData(
const UkmService* ukm_service) { const UkmService* ukm_service) {
...@@ -72,10 +85,10 @@ base::Value UkmDebugDataExtractor::GetStructuredData( ...@@ -72,10 +85,10 @@ base::Value UkmDebugDataExtractor::GetStructuredData(
base::DictionaryValue ukm_data; base::DictionaryValue ukm_data;
ukm_data.SetKey("state", base::Value(ukm_service->recording_enabled_)); ukm_data.SetKey("state", base::Value(ukm_service->recording_enabled_));
ukm_data.SetKey("client_id", ukm_data.SetKey("client_id", UkmDebugDataExtractor::UInt64AsPairOfInt(
base::Value(static_cast<double>(ukm_service->client_id_))); ukm_service->client_id_));
ukm_data.SetKey("session_id", ukm_data.SetKey("session_id",
base::Value(static_cast<double>(ukm_service->session_id_))); base::Value(static_cast<int>(ukm_service->session_id_)));
std::map<SourceId, SourceData> source_data; std::map<SourceId, SourceData> source_data;
for (const auto& kv : ukm_service->recordings_.sources) { for (const auto& kv : ukm_service->recordings_.sources) {
...@@ -93,10 +106,12 @@ base::Value UkmDebugDataExtractor::GetStructuredData( ...@@ -93,10 +106,12 @@ base::Value UkmDebugDataExtractor::GetStructuredData(
base::DictionaryValue source_value; base::DictionaryValue source_value;
if (src) { if (src) {
source_value.SetKey("id", base::Value(static_cast<double>(src->id()))); source_value.SetKey("id",
UkmDebugDataExtractor::UInt64AsPairOfInt(src->id()));
source_value.SetKey("url", base::Value(src->url().spec())); source_value.SetKey("url", base::Value(src->url().spec()));
} else { } else {
source_value.SetKey("id", base::Value(static_cast<double>(kv.first))); source_value.SetKey("id",
UkmDebugDataExtractor::UInt64AsPairOfInt(kv.first));
} }
base::ListValue entries_list; base::ListValue entries_list;
......
...@@ -26,6 +26,13 @@ class UkmDebugDataExtractor { ...@@ -26,6 +26,13 @@ class UkmDebugDataExtractor {
// Returns UKM data structured in a DictionaryValue. // Returns UKM data structured in a DictionaryValue.
static base::Value GetStructuredData(const UkmService* ukm_service); static base::Value GetStructuredData(const UkmService* ukm_service);
// Convert uint64 to pair of int32 to match the spec of Value. JS doesn't
// support uint64 while most of UKM metrics are 64 bit numbers. So,
// they will be passed as a pair of 32 bit ints. The first item is the
// 32 bit representation of the high 32 bit and the second item is the lower
// 32 bit of the 64 bit number.
static base::Value UInt64AsPairOfInt(uint64_t v);
private: private:
DISALLOW_COPY_AND_ASSIGN(UkmDebugDataExtractor); DISALLOW_COPY_AND_ASSIGN(UkmDebugDataExtractor);
}; };
......
/* 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.
*
* This is the stylesheet used by chrome://ukm debug page.
*/
div {
margin: auto;
padding: 2px;
}
div.ukm_collection_status {
background: linear-gradient(0deg, steelblue, lightblue);
padding: 16px;
color: white;
width: auto;
}
div.right_align {
float: right;
width: auto;
}
div.left_align {
display: inline;
}
#sources>div:nth-child(odd) {
background: lightsteelblue;
}
#sources>div:nth-child(even) {
background: lightgrey;
}
span#state, span#clientid {
text-decoration: underline;
font-weight: bold;
}
span.url {
font-weight: bold;
}
span.sourceid {
float: right;
}
div.url_card {
padding: 0px;
}
div.entries {
display: none;
overflow: hidden;
}
div.entry {
width: 470px;
padding: 0px 0px 10px 10px;
display: inline-block;
}
div.collapsible_header {
background-color: #eee;
cursor: pointer;
}
div.collapsible_header:hover {
background-color: #ccc;
}
td {
font-family: "Lucida Console", Monaco, monospace;
font-size: 0.8em;
overflow-wrap: break-word;
border-top: 1px solid black;
}
td.entry_name {
min-width: 320px;
}
td.metric_name {
min-width: 320px;
border-left: 1px solid black;
}
td.metric_value {
text-align: right;
min-width: 120px;
border-left: 1px solid black;
}
table.entry_table {
width: 780px;
border: solid 1px black;
}
select.hex_list {
width: 100px;
font-size: 0.7em;
font-family: "Lucida Console", Monaco, monospace;
height: 22px;
}
div.source_container {
padding: 0px;
}
div.source_container:first-child {
padding-top: 2px;
}
input.text {
width: 480px;
}
input.checkbox {
}
div.labels {
width: 120px;
display: inline-block;
}
...@@ -3,22 +3,52 @@ ...@@ -3,22 +3,52 @@
found in the LICENSE file.--> found in the LICENSE file.-->
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<meta charset="utf-8"> <meta charset="utf-8">
<script src="chrome://resources/js/cr.js"></script> <script src="chrome://resources/js/cr.js"></script>
<script src="chrome://resources/js/promise_resolver.js"></script> <script src="chrome://resources/js/promise_resolver.js"></script>
<script src="chrome://resources/js/util.js"></script> <script src="chrome://resources/js/util.js"></script>
<if expr="is_ios"> <if expr="is_ios">
<!-- TODO(crbug.com/487000): Remove this once injected by web. --> <!-- TODO(crbug.com/487000): Remove this once injected by web. -->
<script src="chrome://resources/js/ios/web_ui.js"></script> <script src="chrome://resources/js/ios/web_ui.js"></script>
</if> </if>
<title>UKM Debug page</title> <link rel="stylesheet" type="text/css" href="ukm_internals.css">
<h1>UKM Debug page</h1> <title>UKM Debug page</title>
<div> <div class="ukm_collection_status">
<p>Is Enabled:<span id="state"></span></p> <div>
<p>Client Id:<span id="clientid"></span></p> <div class="left_align"> Metrics Collection is
<p>Session Id:<span id="sessionid"></span></p> <span id="state"></span>
<h2>Sources</h2> (Session: <span id="sessionid"></span>)
</div>
<div class="right_align">ClientId:
<span id="clientid"/>
</div>
</div>
</div>
<div id="warnings"></div>
<div id="sources"></div> <div id="sources"></div>
</div> <div>
<script src="ukm_internals.js"></script> <div class='labels'> Recorder Ids: </div>
<select id="thread_ids" class="hex_list"></select>
</div>
<div>
<div class='labels'> Metrics Filter: </div>
<input id="metrics_select" type="text" value=".*" class="text" pattern="[A-Za-z0-9\._\*\|\?\+]*" maxlength="256"></input>
</div>
<div>
<div class='labels'> URL Filter: </div>
<input id="url_select" type="text" value=".*" class="text" pattern="[A-Za-z0-9\._\*\|\?\+]*" maxlength="256"></input>
</div>
<div>
<input id="hide_no_metrics" class="checkbox" type="checkbox" checked> Hide Sources With No Metric </input>
</div>
<div>
<input id="hide_no_url" class="checkbox" type="checkbox" checked> Hide Sources With No URL </input>
</div>
<div>
<input id="include_cache" class="checkbox" type="checkbox" checked> Include Cached Sources (since this page load) </input>
</div>
<button id="toggle_expand"></button>
<button id="clear">Clear</button>
<button id="refresh">Refresh</button>
<script src="ukm_internals.js"></script>
</html> </html>
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
/** /**
* @typedef {{ * @typedef {{
* name: string, * name: string,
* value: string * value: !Array<number>
* }} * }}
*/ */
var Metric; let Metric;
/** /**
* @typedef {{ * @typedef {{
...@@ -16,27 +16,313 @@ var Metric; ...@@ -16,27 +16,313 @@ var Metric;
* metrics: !Array<!Metric> * metrics: !Array<!Metric>
* }} * }}
*/ */
var UkmEntry; let UkmEntry;
/** /**
* @typedef {{ * @typedef {{
* url: string, * url: string,
* id: string, * id: !Array<number>,
* entries: !Array<UkmEntry>, * entries: !Array<UkmEntry>,
* }} * }}
*/ */
var UkmDataSource; let UkmDataSource;
/** /**
* The Ukm data sent from the browser. * The Ukm data sent from the browser.
* @typedef {{ * @typedef {{
* state: boolean, * state: boolean,
* client_id: string, * client_id: !Array<number>,
* session_id: string, * session_id: string,
* sources: !Array<!UkmDataSource>, * sources: !Array<!UkmDataSource>,
* }} * }}
*/ */
var UkmData; let UkmData;
/**
* Stores source id and number of entries shown. If there is a new source id
* or there are new entries in Ukm recorder, then all the entries for
* the new source ID will be displayed.
* @type{Map<string, number>}
*/
const ClearedSources = new Map();
/**
* Cached sources to persist beyond the log cut. This will ensure that the data
* on the page don't disappear if there is a log cut. The caching will
* start when the page is loaded and when the data is refreshed.
* Stored data is sourceid -> UkmDataSource with array of distinct entries.
* @type{Map<string, !UkmDataSource>}
*/
const CachedSources = new Map();
/**
* Text for empty url.
* @type {string}
*/
const URL_EMPTY = 'missing';
/**
* Converts a pair of JS 32 bin number to 64 bit hex string. This is used to
* pass 64 bit numbers from UKM like client id and 64 bit metrics to
* the javascript.
* @param {!Array<number>} num A pair of javascript signed int.
* @return {string} unsigned int64 as hex number or a decimal number if the
* value is smaller than 32bit.
*/
function as64Bit(num) {
if (num.length != 2) {
return '0';
}
if (!num[0]) {
return num[1].toString(); // Return the lsb as String.
} else {
const hi = (num[0] >>> 0).toString(16).padStart(8, '0');
const lo = (num[1] >>> 0).toString(16).padStart(8, '0');
return `0x${hi}${lo}`;
}
}
/**
* Sets the display option of all the elements in HtmlCollection to the value
* passed.
* @param {!NodeList<!Element>} collection Collection of Elements.
*/
function setDisplayStyle(collection, display_value) {
for (const el of collection)
el.style.display = display_value;
}
/**
* Remove all the child elements.
* @param {!Element} parent Parent element whose children will get removed.
*/
function removeChildren(parent) {
while (parent.firstChild) {
parent.removeChild(parent.firstChild);
}
}
/**
* Create card for URL.
* @param {!Array<!UkmDataSource>} sourcesForUrl Sources that are for same URL.
* @param {string} url URL or Source id as hex string if the URL is missing.
* @param {!Element} sourcesDiv Sources div where this card will be added to.
* @param {!Map<string, ?string>} displayState Map from source id to value
* of display property of the entries div.
*/
function createUrlCard(sourcesForUrl, url, sourcesDiv, displayState) {
const sourceDiv = createElementWithClassName('div', 'url_card');
sourcesDiv.appendChild(sourceDiv);
if (!sourcesForUrl || sourcesForUrl.length === 0)
return;
for (const source of sourcesForUrl) {
// This div allows hiding of the metrics per URL.
const sourceContainer = /** @type {!Element} */ (createElementWithClassName(
'div', 'source_container'));
sourceDiv.appendChild(sourceContainer);
createUrlHeader(source.url, source.id, sourceContainer);
createSourceCard(
source, sourceContainer, displayState.get(as64Bit(source.id)));
}
}
/**
* Create header containing URL and source ID data.
* @param {?string} url URL.
* @param {!Array<number>} id SourceId as hex.
* @param {!Element} sourceDiv Div under which header will get added.
*/
function createUrlHeader(url, id, sourceDiv) {
const headerElement = createElementWithClassName('div', 'collapsible_header');
sourceDiv.appendChild(headerElement);
const urlElement = createElementWithClassName('span', 'url');
urlElement.innerText = url ? url : URL_EMPTY;
headerElement.appendChild(urlElement);
const idElement = createElementWithClassName('span', 'sourceid');
idElement.innerText = as64Bit(id);
headerElement.appendChild(idElement);
// Make the click on header toggle entries div.
headerElement.addEventListener('click', () => {
const content = headerElement.nextElementSibling;
if (content.style.display === 'block') {
content.style.display = 'none';
} else {
content.style.display = 'block';
}
});
}
/**
* Create a card with UKM Source data.
* @param {!UkmDataSource} source UKM source data.
* @param {!Element} sourceDiv Source div where this card will be added to.
* @param {?string} displayState If display style of this source id is modified
* then the state of the display style.
*/
function createSourceCard(source, sourceDiv, displayState) {
const metricElement =
/** @type {!Element} */ (createElementWithClassName('div', 'entries'));
sourceDiv.appendChild(metricElement);
const sortedEntry =
source.entries.sort((x, y) => x.name.localeCompare(y.name));
for (const entry of sortedEntry) {
createEntryTable(entry, metricElement);
}
if (displayState) {
metricElement.style.display = displayState;
} else {
if ($('toggle_expand').textContent === 'Collapse')
metricElement.style.display = 'block';
else
metricElement.style.display = 'none';
}
}
/**
* Create UKM Entry Table.
* @param {!UkmEntry} entry A Ukm metrics Entry.
* @param {!Element} sourceDiv Element whose children will be the entries.
*/
function createEntryTable(entry, sourceDiv) {
// Add first column to the table.
const entryTable = createElementWithClassName('table', 'entry_table');
entryTable.setAttribute('value', entry.name);
sourceDiv.appendChild(entryTable);
const firstRow = document.createElement('tr');
entryTable.appendChild(firstRow);
const entryName = createElementWithClassName('td', 'entry_name');
entryName.setAttribute('rowspan', 0);
entryName.textContent = entry.name;
firstRow.appendChild(entryName);
// Add metrics columns.
for (const metric of entry.metrics) {
const nextRow = document.createElement('tr');
const metricName = createElementWithClassName('td', 'metric_name');
metricName.textContent = metric.name;
nextRow.appendChild(metricName);
const metricValue = createElementWithClassName('td', 'metric_value');
metricValue.textContent = as64Bit(metric.value);
nextRow.appendChild(metricValue);
entryTable.appendChild(nextRow);
}
}
/**
* Collect all sources for a particular URL together. It will also sort the
* urls alphabetically.
* If the URL field is missing, the source ID will be used as the
* URL for the purpose of grouping and sorting.
* @param {!Array<!UkmDataSource>} sources List of UKM data for a source .
* @return {!Map<string, !Array<!UkmDataSource>>} Mapping in the sorted
* order of URL from URL to list of sources for the URL.
*/
function urlToSourcesMapping(sources) {
const unsorted = new Map();
for (const source of sources) {
const key = source.url ? source.url : as64Bit(source.id);
if (!unsorted.has(key)) {
unsorted.set(key, [source]);
} else {
unsorted.get(key).push(source);
}
}
// Sort the map by URLs.
return new Map(Array.from(unsorted).sort(
(s1,s2) => s1[0].localeCompare(s2[0])));
}
/**
* Adds a button to Expand/Collapse all URLs.
*/
function addExpandToggleButton() {
const toggleExpand = $('toggle_expand');
toggleExpand.textContent = 'Expand';
toggleExpand.addEventListener('click', () => {
if (toggleExpand.textContent == 'Expand') {
toggleExpand.textContent = 'Collapse';
setDisplayStyle(document.getElementsByClassName('entries'), 'block');
} else {
toggleExpand.textContent = 'Expand';
setDisplayStyle(document.getElementsByClassName('entries'), 'none');
}
});
}
/**
* Adds a button to clear all the existing URLs. Note that the hiding is
* done in the UI only. So refreshing the page will show all the UKM again.
* To get the new UKMs after hitting Clear click the refresh button.
*/
function addClearButton() {
const clearButton = $('clear');
clearButton.addEventListener('click', () => {
// Note it won't be able to clear if UKM logs got cut during this call.
cr.sendWithPromise('requestUkmData').then((/** @type {UkmData} */ data) => {
updateUkmCache(data);
for (const s of CachedSources.values())
ClearedSources.set(as64Bit(s.id), s.entries.length);
});
$('toggle_expand').textContent = 'Expand';
updateUkmData();
});
}
/**
* Populate thread ids from the high bit of source id in sources.
* @param {!Array<!UkmDataSource>} sources Array of UKM source.
*/
function populateThreadIds(sources) {
const threadIdSelect = $('thread_ids');
const currentOptions =
new Set(Array.from(threadIdSelect.options).map(o => o.value));
// The first 32 bit of the ID is the recorder ID, convert it to a positive
// bit patterns and then to hex. Ids that were not seen earlier will get
// added to the end of the option list.
const newIds = new Set(sources.map(e => (e.id[0] >>> 0).toString(16)));
const options = ['All', ...Array.from(newIds).sort()];
for (const id of options) {
if (!currentOptions.has(id)) {
const option = document.createElement("option");
option.textContent = id;
option.setAttribute('value', id);
threadIdSelect.add(option);
}
}
}
/**
* This function tries to preserve UKM logs around UKM log uploads. There is
* no way of knowing if duplicate entries for a log are actually produced
* again after the log cut or if they older records since we don't maintain
* timestamp with entries. So only distinct entries will be recorded in the
* cache. i.e if two entries have exactly the same set of metrics then one
* of the entry will not be kept in the cache.
* @param {UkmData} data New UKM data to add to cache.
*/
function updateUkmCache(data) {
for (const source of data.sources) {
const key = as64Bit(source.id);
if (!CachedSources.has(key)) {
const mergedSource = {id: source.id, entries: source.entries};
if (source.url)
mergedSource.url = source.url;
CachedSources.set(key, mergedSource);
} else {
// Merge distinct entries from the source.
const existingEntries =
new Set(CachedSources.get(key).entries.map(e => JSON.stringify(e)));
for (const entry of source.entries) {
if (!existingEntries.has(JSON.stringify(entry))) {
CachedSources.get(key).entries.push(entry);
}
}
}
}
}
/** /**
* Fetches data from the Ukm service and updates the DOM to display it as a * Fetches data from the Ukm service and updates the DOM to display it as a
...@@ -44,35 +330,107 @@ var UkmData; ...@@ -44,35 +330,107 @@ var UkmData;
*/ */
function updateUkmData() { function updateUkmData() {
cr.sendWithPromise('requestUkmData').then((/** @type {UkmData} */ data) => { cr.sendWithPromise('requestUkmData').then((/** @type {UkmData} */ data) => {
$('state').innerText = data.state ? 'True' : 'False'; updateUkmCache(data);
$('clientid').innerText = data.client_id; if ($('include_cache').checked) {
data.sources = [...CachedSources.values()];
}
$('state').innerText = data.state? 'ENABLED' : 'DISABLED';
$('clientid').innerText = as64Bit(data.client_id);
$('sessionid').innerText = data.session_id; $('sessionid').innerText = data.session_id;
let sourceDiv = $('sources'); const sourcesDiv = /** @type {!Element} */ ($('sources'));
for (const source of data.sources) { const currentDisplayState = new Map();
const sourceElement = document.createElement('h3'); for (const el of document.getElementsByClassName('source_container')) {
if (source.url !== undefined) currentDisplayState.set(el.querySelector('.sourceid').textContent,
sourceElement.innerText = `Id: ${source.id} Url: ${source.url}`; el.querySelector('.entries').style.display);
else }
sourceElement.innerText = `Id: ${source.id}`; removeChildren(sourcesDiv);
sourceDiv.appendChild(sourceElement); const urlToSources = urlToSourcesMapping(
filterSourcesUsingFormOptions(data.sources));
for (const url of urlToSources.keys()) {
createUrlCard(
urlToSources.get(url), url, sourcesDiv, currentDisplayState);
}
populateThreadIds(data.sources);
});
}
for (const entry of source.entries) { /**
const entryElement = document.createElement('h4'); * Filter sources that have been recorded previously. If it sees a source id
entryElement.innerText = `Entry: ${entry.name}`; * where number of entries has decreased then it will add a warning.
sourceDiv.appendChild(entryElement); * @param {!Array<!UkmDataSource>} sources All the sources currently in
* UKM recorder.
* @return {!Array<!UkmDataSource>} Sources which are new or have a new entry
* logged for them.
*/
function filterSourcesUsingFormOptions(sources) {
// Filter sources based on if they have been cleared.
const newSources = sources.filter(source => (
// Keep sources if it is newly generated since clearing earlier.
!ClearedSources.has(as64Bit(source.id)) ||
// Keep sources if it has increased entities since clearing earlier.
(source.entries.length > ClearedSources.get(as64Bit(source.id)))
));
if (entry.metrics === undefined) // Applies the filter from Metrics selector.
continue; const newSourcesWithEntriesCleared = newSources.map(source => {
for (const metric of entry.metrics) { const metricsFilterValue = $('metrics_select').value;
const metricElement = document.createElement('h5'); if (metricsFilterValue) {
metricElement.innerText = const metricsRe = new RegExp(metricsFilterValue);
`Metric: ${metric.name} Value: ${metric.value}`; source.entries = source.entries.filter(e => metricsRe.test(e.name));
sourceDiv.appendChild(metricElement);
}
} }
return source;
});
// Filter sources based on the status of check-boxes.
const filteredSources = newSourcesWithEntriesCleared.filter(source => (
(!$('hide_no_url').checked || source.url) &&
(!$('hide_no_metrics').checked || source.entries.length)
));
// Filter sources based on thread id.
const threadsFilteredSource = filteredSources.filter(source => {
const selectedOption =
$('thread_ids').options[$('thread_ids').selectedIndex];
return !selectedOption ||
(selectedOption.value === 'All') ||
((source.id[0] >>> 0).toString() === selectedOption.value);
});
// Filter URLs based on URL selector input.
return threadsFilteredSource.filter(source => {
const urlFilterValue = $('url_select').value;
if (urlFilterValue) {
const urlRe = new RegExp(urlFilterValue);
// Will also match missing URLs by default.
return !source.url || urlRe.test(source.url);
} }
return true;
}); });
} }
document.addEventListener('DOMContentLoaded', updateUkmData); /**
* DomContentLoaded handler.
*/
function onLoad() {
addExpandToggleButton();
addClearButton();
updateUkmData();
$('refresh').addEventListener('click', updateUkmData);
$('hide_no_metrics').addEventListener('click', updateUkmData);
$('hide_no_url').addEventListener('click', updateUkmData);
$('thread_ids').addEventListener('click', updateUkmData);
$('include_cache').addEventListener('click', updateUkmData);
$('metrics_select').addEventListener('keyup', e => {
if (e.key === 'Enter')
updateUkmData();
});
$('url_select').addEventListener('keyup', e => {
if (e.key === 'Enter')
updateUkmData();
});
}
document.addEventListener('DOMContentLoaded', onLoad);
setInterval(updateUkmData, 120000); // Refresh every 2 minutes.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment