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: Jochen Eisinger <jochen@chromium.org>
Reviewed-by: calamity <calamity@chromium.org>
Reviewed-by: Steven Holte <holte@chromium.org>
Reviewed-by: Mikel Astiz <mastiz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#606660}
diff --git a/BUILD.gn b/BUILD.gn
index 68fa462..e4d8f64d 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1223,6 +1223,7 @@
"chrome/browser/resources:closure_compile",
"content/browser/resources:closure_compile",
"ui/webui/resources:closure_compile",
+ "components/ukm/debug:closure_compile",
]
if (is_chromeos) {
data_deps += [ "ui/file_manager:closure_compile" ]
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/sync/UkmTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/sync/UkmTest.java
index 43bee7a..134ef80 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/sync/UkmTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/sync/UkmTest.java
@@ -69,8 +69,8 @@
public boolean isUkmEnabled(Tab normalTab) throws Exception {
String state = getElementContent(normalTab, "state");
Assert.assertTrue(
- "UKM state: " + state, state.equals("\"True\"") || state.equals("\"False\""));
- return state.equals("\"True\"");
+ "UKM state: " + state, state.equals("\"ENABLED\"") || state.equals("\"DISABLED\""));
+ return state.equals("\"ENABLED\"");
}
public String getUkmClientId(Tab normalTab) throws Exception {
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd
index e726146..2a91fce 100644
--- a/chrome/browser/browser_resources.grd
+++ b/chrome/browser/browser_resources.grd
@@ -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_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_CSS" file="../../components/ukm/debug/ukm_internals.css" flattenhtml="true" compress="gzip" type="BINDATA" />
<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_2X_INCOGNITO_MARKER_PNG" file="resources\md_downloads\2x\incognito_marker.png" type="BINDATA" />
diff --git a/chrome/browser/ui/webui/ukm/ukm_internals_ui.cc b/chrome/browser/ui/webui/ukm/ukm_internals_ui.cc
index 0b69b7b..1823905 100644
--- a/chrome/browser/ui/webui/ukm/ukm_internals_ui.cc
+++ b/chrome/browser/ui/webui/ukm/ukm_internals_ui.cc
@@ -29,6 +29,7 @@
content::WebUIDataSource::Create(chrome::kChromeUIUkmHost);
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->UseGzip();
return source;
diff --git a/components/ukm/debug/.eslintrc.js b/components/ukm/debug/.eslintrc.js
new file mode 100644
index 0000000..baeb4a6c
--- /dev/null
+++ b/components/ukm/debug/.eslintrc.js
@@ -0,0 +1,11 @@
+// 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',
+ },
+};
+
diff --git a/components/ukm/debug/PRESUBMIT.py b/components/ukm/debug/PRESUBMIT.py
new file mode 100644
index 0000000..2bc036d
--- /dev/null
+++ b/components/ukm/debug/PRESUBMIT.py
@@ -0,0 +1,20 @@
+# 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)
+
diff --git a/components/ukm/debug/ukm_debug_data_extractor.cc b/components/ukm/debug/ukm_debug_data_extractor.cc
index 4877914..f26e9e4 100644
--- a/components/ukm/debug/ukm_debug_data_extractor.cc
+++ b/components/ukm/debug/ukm_debug_data_extractor.cc
@@ -20,6 +20,8 @@
namespace {
+static const uint64_t BIT_FILTER_LAST32 = 0xffffffffULL;
+
struct SourceData {
UkmSource* source;
std::vector<mojom::UkmEntry*> entries;
@@ -38,8 +40,8 @@
const auto it = decode_map.find(entry.event_hash);
if (it == decode_map.end()) {
- entry_value.SetKey("name",
- base::Value(static_cast<double>(entry.event_hash)));
+ entry_value.SetKey(
+ "name", UkmDebugDataExtractor::UInt64AsPairOfInt(entry.event_hash));
} else {
entry_value.SetKey("name", base::Value(it->second.name));
@@ -49,8 +51,8 @@
base::DictionaryValue metric_value;
metric_value.SetKey("name",
base::Value(GetName(it->second, metric.first)));
- metric_value.SetKey("value",
- base::Value(static_cast<double>(metric.second)));
+ metric_value.SetKey(
+ "value", UkmDebugDataExtractor::UInt64AsPairOfInt(metric.second));
metrics_list_storage->push_back(std::move(metric_value));
}
entry_value.SetKey("metrics", std::move(metrics_list));
@@ -65,6 +67,17 @@
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
base::Value UkmDebugDataExtractor::GetStructuredData(
const UkmService* ukm_service) {
if (!ukm_service)
@@ -72,10 +85,10 @@
base::DictionaryValue ukm_data;
ukm_data.SetKey("state", base::Value(ukm_service->recording_enabled_));
- ukm_data.SetKey("client_id",
- base::Value(static_cast<double>(ukm_service->client_id_)));
+ ukm_data.SetKey("client_id", UkmDebugDataExtractor::UInt64AsPairOfInt(
+ ukm_service->client_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;
for (const auto& kv : ukm_service->recordings_.sources) {
@@ -93,10 +106,12 @@
base::DictionaryValue source_value;
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()));
} else {
- source_value.SetKey("id", base::Value(static_cast<double>(kv.first)));
+ source_value.SetKey("id",
+ UkmDebugDataExtractor::UInt64AsPairOfInt(kv.first));
}
base::ListValue entries_list;
diff --git a/components/ukm/debug/ukm_debug_data_extractor.h b/components/ukm/debug/ukm_debug_data_extractor.h
index dc2810d..9d06209 100644
--- a/components/ukm/debug/ukm_debug_data_extractor.h
+++ b/components/ukm/debug/ukm_debug_data_extractor.h
@@ -26,6 +26,13 @@
// Returns UKM data structured in a DictionaryValue.
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:
DISALLOW_COPY_AND_ASSIGN(UkmDebugDataExtractor);
};
diff --git a/components/ukm/debug/ukm_internals.css b/components/ukm/debug/ukm_internals.css
new file mode 100644
index 0000000..e8efc0d
--- /dev/null
+++ b/components/ukm/debug/ukm_internals.css
@@ -0,0 +1,127 @@
+/* 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;
+}
diff --git a/components/ukm/debug/ukm_internals.html b/components/ukm/debug/ukm_internals.html
index e8ca44d0..0d6414ab 100644
--- a/components/ukm/debug/ukm_internals.html
+++ b/components/ukm/debug/ukm_internals.html
@@ -3,22 +3,52 @@
found in the LICENSE file.-->
<!doctype html>
<html lang="en">
-<meta charset="utf-8">
-<script src="chrome://resources/js/cr.js"></script>
-<script src="chrome://resources/js/promise_resolver.js"></script>
-<script src="chrome://resources/js/util.js"></script>
-<if expr="is_ios">
- <!-- TODO(crbug.com/487000): Remove this once injected by web. -->
- <script src="chrome://resources/js/ios/web_ui.js"></script>
-</if>
-<title>UKM Debug page</title>
-<h1>UKM Debug page</h1>
-<div>
- <p>Is Enabled:<span id="state"></span></p>
- <p>Client Id:<span id="clientid"></span></p>
- <p>Session Id:<span id="sessionid"></span></p>
- <h2>Sources</h2>
+ <meta charset="utf-8">
+ <script src="chrome://resources/js/cr.js"></script>
+ <script src="chrome://resources/js/promise_resolver.js"></script>
+ <script src="chrome://resources/js/util.js"></script>
+ <if expr="is_ios">
+ <!-- TODO(crbug.com/487000): Remove this once injected by web. -->
+ <script src="chrome://resources/js/ios/web_ui.js"></script>
+ </if>
+ <link rel="stylesheet" type="text/css" href="ukm_internals.css">
+ <title>UKM Debug page</title>
+ <div class="ukm_collection_status">
+ <div>
+ <div class="left_align"> Metrics Collection is
+ <span id="state"></span>
+ (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>
-<script src="ukm_internals.js"></script>
+ <div>
+ <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>
diff --git a/components/ukm/debug/ukm_internals.js b/components/ukm/debug/ukm_internals.js
index e259e2a..55434b2 100644
--- a/components/ukm/debug/ukm_internals.js
+++ b/components/ukm/debug/ukm_internals.js
@@ -5,10 +5,10 @@
/**
* @typedef {{
* name: string,
- * value: string
+ * value: !Array<number>
* }}
*/
-var Metric;
+let Metric;
/**
* @typedef {{
@@ -16,27 +16,313 @@
* metrics: !Array<!Metric>
* }}
*/
-var UkmEntry;
+let UkmEntry;
/**
* @typedef {{
* url: string,
- * id: string,
+ * id: !Array<number>,
* entries: !Array<UkmEntry>,
* }}
*/
-var UkmDataSource;
+let UkmDataSource;
/**
* The Ukm data sent from the browser.
* @typedef {{
* state: boolean,
- * client_id: string,
+ * client_id: !Array<number>,
* session_id: string,
* 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
@@ -44,35 +330,107 @@
*/
function updateUkmData() {
cr.sendWithPromise('requestUkmData').then((/** @type {UkmData} */ data) => {
- $('state').innerText = data.state ? 'True' : 'False';
- $('clientid').innerText = data.client_id;
+ updateUkmCache(data);
+ 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;
- let sourceDiv = $('sources');
- for (const source of data.sources) {
- const sourceElement = document.createElement('h3');
- if (source.url !== undefined)
- sourceElement.innerText = `Id: ${source.id} Url: ${source.url}`;
- else
- sourceElement.innerText = `Id: ${source.id}`;
- sourceDiv.appendChild(sourceElement);
-
- for (const entry of source.entries) {
- const entryElement = document.createElement('h4');
- entryElement.innerText = `Entry: ${entry.name}`;
- sourceDiv.appendChild(entryElement);
-
- if (entry.metrics === undefined)
- continue;
- for (const metric of entry.metrics) {
- const metricElement = document.createElement('h5');
- metricElement.innerText =
- `Metric: ${metric.name} Value: ${metric.value}`;
- sourceDiv.appendChild(metricElement);
- }
- }
+ const sourcesDiv = /** @type {!Element} */ ($('sources'));
+ const currentDisplayState = new Map();
+ for (const el of document.getElementsByClassName('source_container')) {
+ currentDisplayState.set(el.querySelector('.sourceid').textContent,
+ el.querySelector('.entries').style.display);
}
+ removeChildren(sourcesDiv);
+ const urlToSources = urlToSourcesMapping(
+ filterSourcesUsingFormOptions(data.sources));
+ for (const url of urlToSources.keys()) {
+ createUrlCard(
+ urlToSources.get(url), url, sourcesDiv, currentDisplayState);
+ }
+ populateThreadIds(data.sources);
});
}
-document.addEventListener('DOMContentLoaded', updateUkmData);
+/**
+ * Filter sources that have been recorded previously. If it sees a source id
+ * where number of entries has decreased then it will add a warning.
+ * @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)))
+ ));
+
+ // Applies the filter from Metrics selector.
+ const newSourcesWithEntriesCleared = newSources.map(source => {
+ const metricsFilterValue = $('metrics_select').value;
+ if (metricsFilterValue) {
+ const metricsRe = new RegExp(metricsFilterValue);
+ source.entries = source.entries.filter(e => metricsRe.test(e.name));
+ }
+ 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;
+ });
+}
+
+/**
+ * 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.