Commit 9fa63c8d authored by Becca Hughes's avatar Becca Hughes Committed by Commit Bot

[Media History] Add origin table to WebUI

Add the output of the origin table to the
WebUI for debugging.

BUG=1024353

Change-Id: I2eb8e99d94bf8792bd459529ae5aae3bacffe89d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2037642Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Reviewed-by: default avatarTommy Steimel <steimel@chromium.org>
Commit-Queue: Becca Hughes <beccahughes@chromium.org>
Cr-Commit-Position: refs/heads/master@{#739088}
parent 4e492136
......@@ -63,6 +63,8 @@ class MediaHistoryStoreInternal
mojom::MediaHistoryStatsPtr GetMediaHistoryStats();
int GetTableRowCount(const std::string& table_name);
std::vector<mojom::MediaHistoryOriginRowPtr> GetOriginRowsForDebug();
void SavePlaybackSession(
const GURL& url,
const media_session::MediaMetadata& metadata,
......@@ -220,6 +222,46 @@ mojom::MediaHistoryStatsPtr MediaHistoryStoreInternal::GetMediaHistoryStats() {
return stats;
}
std::vector<mojom::MediaHistoryOriginRowPtr>
MediaHistoryStoreInternal::GetOriginRowsForDebug() {
std::vector<mojom::MediaHistoryOriginRowPtr> origins;
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!initialization_successful_)
return origins;
sql::Statement statement(DB()->GetUniqueStatement(
base::StringPrintf(
"SELECT O.origin, O.last_updated_time_s, "
"O.aggregate_watchtime_audio_video_s, "
"(SELECT SUM(watch_time_s) FROM %s WHERE origin_id = O.id AND "
"has_video = 1 AND has_audio = 1) AS accurate_watchtime "
"FROM %s O",
MediaHistoryPlaybackTable::kTableName,
MediaHistoryOriginTable::kTableName)
.c_str()));
std::vector<std::string> table_names;
while (statement.Step()) {
mojom::MediaHistoryOriginRowPtr origin(mojom::MediaHistoryOriginRow::New());
origin->origin = url::Origin::Create(GURL(statement.ColumnString(0)));
origin->last_updated_time =
base::Time::FromDeltaSinceWindowsEpoch(
base::TimeDelta::FromSeconds(statement.ColumnInt64(1)))
.ToJsTime();
origin->cached_audio_video_watchtime =
base::TimeDelta::FromSeconds(statement.ColumnInt64(2));
origin->actual_audio_video_watchtime =
base::TimeDelta::FromSeconds(statement.ColumnInt64(3));
origins.push_back(std::move(origin));
}
DCHECK(statement.Succeeded());
return origins;
}
int MediaHistoryStoreInternal::GetTableRowCount(const std::string& table_name) {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!initialization_successful_)
......@@ -328,6 +370,20 @@ void MediaHistoryStore::GetMediaHistoryStats(
std::move(callback));
}
void MediaHistoryStore::GetOriginRowsForDebug(
base::OnceCallback<void(std::vector<mojom::MediaHistoryOriginRowPtr>)>
callback) {
if (!db_->initialization_successful_) {
return std::move(callback).Run(
std::vector<mojom::MediaHistoryOriginRowPtr>());
}
base::PostTaskAndReplyWithResult(
db_->db_task_runner_.get(), FROM_HERE,
base::BindOnce(&MediaHistoryStoreInternal::GetOriginRowsForDebug, db_),
std::move(callback));
}
void MediaHistoryStore::SavePlaybackSession(
const GURL& url,
const media_session::MediaMetadata& metadata,
......
......@@ -58,6 +58,12 @@ class MediaHistoryStore {
void GetMediaHistoryStats(
base::OnceCallback<void(mojom::MediaHistoryStatsPtr)> callback);
// Returns all the rows in the origin table. This should only be used for
// debugging because it is very slow.
void GetOriginRowsForDebug(
base::OnceCallback<void(std::vector<mojom::MediaHistoryOriginRowPtr>)>
callback);
// Gets the playback sessions from the media history store. The results will
// be ordered by most recent first and be limited to the first |num_sessions|.
// For each session it calls |filter| and if that returns |true| then that
......
......@@ -4,15 +4,34 @@
module media_history.mojom;
import "mojo/public/mojom/base/time.mojom";
import "url/mojom/origin.mojom";
struct MediaHistoryStats {
// The row count of the different tables. The key is the table name and the
// value is the row count.
map<string, int32> table_row_counts;
};
struct MediaHistoryOriginRow {
url.mojom.Origin origin;
// The total audio+video watchtime for the origin. The cached value is cached
// on the origin row for speed and the actual is calculated by adding up
// the playback rows.
mojo_base.mojom.TimeDelta cached_audio_video_watchtime;
mojo_base.mojom.TimeDelta actual_audio_video_watchtime;
// The last updated time of the row in JS time.
double last_updated_time;
};
// MediaHistoryStore allows the Media History WebUI to access data from the
// Media History database for diagnostic purposes.
interface MediaHistoryStore {
// Gets stats about the Media History database.
GetMediaHistoryStats() => (MediaHistoryStats stats);
// Returns the rows from the origin table.
GetMediaHistoryOriginRows() => (array<MediaHistoryOriginRow> rows);
};
......@@ -25,6 +25,14 @@
namespace media_history {
namespace {
// The error margin for double time comparison. It is 10 seconds because it
// might be equal but it might be close too.
const int kTimeErrorMargin = 10000;
} // namespace
class MediaHistoryStoreUnitTest : public testing::Test {
public:
MediaHistoryStoreUnitTest() = default;
......@@ -69,6 +77,20 @@ class MediaHistoryStoreUnitTest : public testing::Test {
return stats_out;
}
std::vector<mojom::MediaHistoryOriginRowPtr> GetOriginRowsSync() {
base::RunLoop run_loop;
std::vector<mojom::MediaHistoryOriginRowPtr> out;
GetMediaHistoryStore()->GetOriginRowsForDebug(base::BindLambdaForTesting(
[&](std::vector<mojom::MediaHistoryOriginRowPtr> rows) {
out = std::move(rows);
run_loop.Quit();
}));
run_loop.Run();
return out;
}
MediaHistoryStore* GetMediaHistoryStore() {
return media_history_store_.get();
}
......@@ -234,6 +256,8 @@ TEST_F(MediaHistoryStoreUnitTest, SavePlayback_IncrementAggregateWatchtime) {
GURL url("http://google.com/test");
GURL url_alt("http://example.org/test");
const auto url_now_before = base::Time::Now().ToJsTime();
{
// Record a watchtime for audio/video for 30 seconds.
content::MediaPlayerWatchTime watch_time(
......@@ -261,9 +285,6 @@ TEST_F(MediaHistoryStoreUnitTest, SavePlayback_IncrementAggregateWatchtime) {
content::RunAllTasksUntilIdle();
}
const int64_t url_now_in_seconds_before =
base::Time::Now().ToDeltaSinceWindowsEpoch().InSeconds();
{
// Record a video-only watchtime for 30 seconds.
content::MediaPlayerWatchTime watch_time(
......@@ -273,8 +294,7 @@ TEST_F(MediaHistoryStoreUnitTest, SavePlayback_IncrementAggregateWatchtime) {
content::RunAllTasksUntilIdle();
}
const int64_t url_now_in_seconds_after =
base::Time::Now().ToDeltaSinceWindowsEpoch().InSeconds();
const auto url_now_after = base::Time::Now().ToJsTime();
{
// Record a watchtime for audio/video for 60 seconds on a different origin.
......@@ -285,8 +305,7 @@ TEST_F(MediaHistoryStoreUnitTest, SavePlayback_IncrementAggregateWatchtime) {
content::RunAllTasksUntilIdle();
}
const int64_t url_alt_now_in_seconds_after =
base::Time::Now().ToDeltaSinceWindowsEpoch().InSeconds();
const auto url_alt_after = base::Time::Now().ToJsTime();
{
// Check the playbacks were recorded.
......@@ -296,26 +315,24 @@ TEST_F(MediaHistoryStoreUnitTest, SavePlayback_IncrementAggregateWatchtime) {
stats->table_row_counts[MediaHistoryPlaybackTable::kTableName]);
}
// Verify that the origin table has the correct aggregate watchtime in
// minutes.
sql::Statement s(GetDB().GetUniqueStatement(
"SELECT origin, aggregate_watchtime_audio_video_s, last_updated_time_s "
"FROM origin"));
ASSERT_TRUE(s.is_valid());
EXPECT_TRUE(s.Step());
EXPECT_EQ("http://google.com/", s.ColumnString(0));
EXPECT_EQ(90, s.ColumnInt64(1));
EXPECT_LE(url_now_in_seconds_before, s.ColumnInt64(2));
EXPECT_GE(url_now_in_seconds_after, s.ColumnInt64(2));
EXPECT_TRUE(s.Step());
EXPECT_EQ("http://example.org/", s.ColumnString(0));
EXPECT_EQ(30, s.ColumnInt64(1));
EXPECT_LE(url_now_in_seconds_after, s.ColumnInt64(2));
EXPECT_GE(url_alt_now_in_seconds_after, s.ColumnInt64(2));
EXPECT_FALSE(s.Step());
std::vector<mojom::MediaHistoryOriginRowPtr> origins = GetOriginRowsSync();
EXPECT_EQ(2u, origins.size());
EXPECT_EQ("http://google.com", origins[0]->origin.Serialize());
EXPECT_EQ(base::TimeDelta::FromSeconds(90),
origins[0]->cached_audio_video_watchtime);
EXPECT_NEAR(url_now_before, origins[0]->last_updated_time, kTimeErrorMargin);
EXPECT_GE(url_now_after, origins[0]->last_updated_time);
EXPECT_EQ(origins[0]->cached_audio_video_watchtime,
origins[0]->actual_audio_video_watchtime);
EXPECT_EQ("http://example.org", origins[1]->origin.Serialize());
EXPECT_EQ(base::TimeDelta::FromSeconds(30),
origins[1]->cached_audio_video_watchtime);
EXPECT_NEAR(url_now_before, origins[1]->last_updated_time, kTimeErrorMargin);
EXPECT_GE(url_alt_after, origins[1]->last_updated_time);
EXPECT_EQ(origins[1]->cached_audio_video_watchtime,
origins[1]->actual_audio_video_watchtime);
}
} // namespace media_history
......@@ -7,7 +7,10 @@
<script src="chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js"></script>
<script src="chrome://resources/js/promise_resolver.js"></script>
<script src="chrome://resources/js/util.js"></script>
<script src="chrome://resources/mojo/mojo/public/mojom/base/unguessable_token.mojom-lite.js"></script>
<script src="chrome://resources/mojo/mojo/public/mojom/base/time.mojom-lite.js"></script>
<script src="chrome://resources/mojo/url/mojom/url.mojom-lite.js"></script>
<script src="chrome://resources/mojo/url/mojom/origin.mojom-lite.js"></script>
<script src="chrome/browser/media/history/media_history_store.mojom-lite.js">
</script>
......@@ -46,6 +49,20 @@
.name-cell {
background-color: rgba(230, 230, 230, 0.5);
}
th.sort-column {
padding-inline-end: 16px;
}
th.sort-column::after {
content: '▲';
position: absolute;
}
th[sort-reverse].sort-column::after {
content: '▼';
position: absolute;
}
</style>
</head>
<body>
......@@ -65,7 +82,35 @@
<tbody id="stats-table-body">
</tbody>
</table>
<table>
<thead>
<tr id="data-table-header">
<th sort-key="origin">
Origin
</th>
<th sort-key="lastUpdatedTime" sort-reverse>
Last Updated
</th>
<th sort-key="cachedAudioVideoWatchtime" class="sort-column" sort-reverse>
Audio + Video Watchtime (secs, cached)
</th>
<th sort-key="actualAudioVideoWatchtime" sort-reverse>
Audio + Video Watchtime (secs, actual)
</th>
</tr>
</thead>
<tbody id="data-table-body">
</tbody>
</table>
<template id="data-row">
<tr>
<td class="origin-cell"></td>
<td class="last-updated-cell"></td>
<td class="cached-watchtime-cell"></td>
<td class="actual-watchtime-cell"></td>
</tr>
</template>
<template id="stats-row">
<tr>
<td class="name-cell"></td>
......
......@@ -7,12 +7,25 @@
// Allow a function to be provided by tests, which will be called when
// the page has been populated with media history details.
const pageIsPopulatedResolver = new PromiseResolver();
let pageIsPopulatedCounter = 0;
function whenPageIsPopulatedForTest() {
return pageIsPopulatedResolver.promise;
}
function maybeResolvePageIsPopulated() {
pageIsPopulatedCounter--;
if (pageIsPopulatedCounter == 0) {
pageIsPopulatedResolver.resolve();
}
}
(function() {
let data = null;
let dataTableBody = null;
let sortReverse = true;
let sortKey = 'cachedAudioVideoWatchtime';
let store = null;
let statsTableBody = null;
......@@ -30,11 +43,100 @@ function createStatsRow(name, count) {
return document.importNode(template.content, true);
}
/**
* Remove all rows from the data table.
*/
function clearDataTable() {
dataTableBody.innerHTML = '';
}
/**
* Compares two MediaHistoryOriginRow objects based on |sortKey|.
* @param {string} sortKey The name of the property to sort by.
* @param {number|mojo_base.mojom.TimeDelta|url.mojom.Origin} The first object
* to compare.
* @param {number|mojo_base.mojom.TimeDelta|url.mojom.Origin} The second object
* to compare.
* @return {number} A negative number if |a| should be ordered before
* |b|, a positive number otherwise.
*/
function compareTableItem(sortKey, a, b) {
const val1 = a[sortKey];
const val2 = b[sortKey];
// Compare the hosts of the origin ignoring schemes.
if (sortKey == 'origin') {
return val1.host > val2.host ? 1 : -1;
}
// Compare mojo_base.mojom.TimeDelta microseconds value.
if (sortKey == 'cachedAudioVideoWatchtime' ||
sortKey == 'actualAudioVideoWatchtime') {
return val1.microseconds - val2.microseconds;
}
if (sortKey == 'lastUpdatedTime') {
return val1 - val2;
}
assertNotReached('Unsupported sort key: ' + sortKey);
return 0;
}
/**
* Sort the data based on |sortKey| and |sortReverse|.
*/
function sortData() {
data.sort((a, b) => {
return (sortReverse ? -1 : 1) * compareTableItem(sortKey, a, b);
});
}
/**
* Regenerates the data table from |data|.
*/
function renderDataTable() {
clearDataTable();
sortData();
data.forEach(rowInfo => dataTableBody.appendChild(createDataRow(rowInfo)));
}
/**
* @param {mojo_base.mojom.TimeDelta} The time delta to format.
* @return {string} The time in seconds with commas added.
*/
function formatWatchtime(time) {
const secs = (time.microseconds / 1000000);
return secs.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
}
/**
* Creates a single row in the data table.
* @param {!MediaHistoryOriginRow} data The data to create the row.
* @return {!HTMLElement}
*/
function createDataRow(data) {
const template = $('data-row');
const td = template.content.querySelectorAll('td');
td[0].textContent = data.origin.scheme + '://' + data.origin.host;
if (data.origin.scheme == 'http' && data.origin.port != '80') {
td[0].textContent += ':' + data.origin.port;
} else if (data.origin.scheme == 'https' && data.origin.port != '443') {
td[0].textContent += ':' + data.origin.port;
}
td[1].textContent =
data.lastUpdatedTime ? new Date(data.lastUpdatedTime).toISOString() : '';
td[2].textContent = formatWatchtime(data.cachedAudioVideoWatchtime);
td[3].textContent = formatWatchtime(data.actualAudioVideoWatchtime);
return document.importNode(template.content, true);
}
/**
* Regenerates the stats table.
* @param {!MediaHistoryStats} stats The stats for the Media History store.
*/
function renderStatsTable(stats) {
statsTableBody.innerHTML = '';
......@@ -47,18 +149,53 @@ function renderStatsTable(stats) {
* Retrieve stats from the backend and then render the table.
*/
function updateTable() {
pageIsPopulatedCounter += 2;
// Populate stats table.
store.getMediaHistoryStats().then(response => {
renderStatsTable(response.stats);
pageIsPopulatedResolver.resolve();
maybeResolvePageIsPopulated();
});
// Populate origin table.
store.getMediaHistoryOriginRows().then(response => {
data = response.rows;
renderDataTable();
maybeResolvePageIsPopulated();
});
}
document.addEventListener('DOMContentLoaded', function() {
store = mediaHistory.mojom.MediaHistoryStore.getRemote();
dataTableBody = $('data-table-body');
statsTableBody = $('stats-table-body');
updateTable();
// Set table header sort handlers.
const dataTableHeader = $('data-table-header');
const headers = dataTableHeader.children;
for (let i = 0; i < headers.length; i++) {
headers[i].addEventListener('click', (e) => {
const newSortKey = e.target.getAttribute('sort-key');
if (sortKey == newSortKey) {
sortReverse = !sortReverse;
} else {
sortKey = newSortKey;
sortReverse = false;
}
const oldSortColumn = document.querySelector('.sort-column');
oldSortColumn.classList.remove('sort-column');
e.target.classList.add('sort-column');
if (sortReverse) {
e.target.setAttribute('sort-reverse', '');
} else {
e.target.removeAttribute('sort-reverse');
}
renderDataTable();
});
}
// Add handler to 'copy all to clipboard' button
const copyAllToClipboardButton = $('copy-all-to-clipboard');
copyAllToClipboardButton.addEventListener('click', (e) => {
......
......@@ -47,6 +47,11 @@ void MediaHistoryUI::GetMediaHistoryStats(
return GetMediaHistoryStore()->GetMediaHistoryStats(std::move(callback));
}
void MediaHistoryUI::GetMediaHistoryOriginRows(
GetMediaHistoryOriginRowsCallback callback) {
return GetMediaHistoryStore()->GetOriginRowsForDebug(std::move(callback));
}
media_history::MediaHistoryStore* MediaHistoryUI::GetMediaHistoryStore() {
Profile* profile = Profile::FromWebUI(web_ui());
DCHECK(profile);
......
......@@ -30,6 +30,8 @@ class MediaHistoryUI : public ui::MojoWebUIController,
// media::mojom::MediaHistoryStore:
void GetMediaHistoryStats(GetMediaHistoryStatsCallback callback) override;
void GetMediaHistoryOriginRows(
GetMediaHistoryOriginRowsCallback callback) override;
private:
media_history::MediaHistoryStore* GetMediaHistoryStore();
......
......@@ -39,10 +39,9 @@ TEST_F('MediaHistoryWebUIBrowserTest', 'MAYBE_All', function() {
return whenPageIsPopulatedForTest();
});
test('check stats table is loaded', function() {
test('check stats table is loaded', () => {
let statsRows =
Array.from(document.getElementById('stats-table-body').children);
assertEquals(4, statsRows.length);
assertDeepEquals(
[
......@@ -53,5 +52,17 @@ TEST_F('MediaHistoryWebUIBrowserTest', 'MAYBE_All', function() {
x => [x.children[0].textContent, x.children[1].textContent]));
});
test('check data table is loaded', () => {
let dataHeaderRows =
Array.from(document.getElementById('data-table-header').children);
assertDeepEquals(
[
'Origin', 'Last Updated', 'Audio + Video Watchtime (secs, cached)',
'Audio + Video Watchtime (secs, actual)'
],
dataHeaderRows.map(x => x.textContent.trim()));
});
mocha.run();
});
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