0

[Storage] Blob Storage perf tests

BUG=489799
CQ_EXTRA_TRYBOTS=tryserver.chromium.perf:linux_perf_bisect;tryserver.chromium.perf:mac_perf_bisect;tryserver.chromium.perf:win_perf_bisect;tryserver.chromium.perf:android_nexus5_perf_bisect

Review URL: https://codereview.chromium.org/1104053006

Cr-Commit-Position: refs/heads/master@{#332541}
This commit is contained in:
dmurph
2015-06-02 20:24:40 -07:00
committed by Commit bot
parent 24b9c277b2
commit dbd2315e79
7 changed files with 560 additions and 2 deletions

@ -159,7 +159,7 @@ void BlobURLRequestJob::SetExtraRequestHeaders(
BlobURLRequestJob::~BlobURLRequestJob() {
STLDeleteValues(&index_to_reader_);
TRACE_EVENT_ASYNC_END1("Blob", "Request", this, "uuid",
TRACE_EVENT_ASYNC_END1("Blob", "BlobRequest", this, "uuid",
blob_data_ ? blob_data_->uuid() : "NotFound");
}

@ -0,0 +1,42 @@
# Copyright 2015 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.
# Copyright 2015 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.
from core import perf_benchmark
from telemetry.core.platform import tracing_category_filter
from telemetry.web_perf import timeline_based_measurement
import page_sets
BLOB_CATEGORY = 'Blob'
TIMELINE_REQUIRED_CATEGORY = 'blink.console'
class BlobStorage(perf_benchmark.PerfBenchmark):
"""Timeline based measurement benchmark for Blob Storage."""
page_set = page_sets.BlobWorkshopPageSet
def CreateTimelineBasedMeasurementOptions(self):
cat_filter = tracing_category_filter.CreateMinimalOverheadFilter()
cat_filter.AddIncludedCategory(BLOB_CATEGORY)
cat_filter.AddIncludedCategory(TIMELINE_REQUIRED_CATEGORY)
return timeline_based_measurement.Options(
overhead_level=cat_filter)
@classmethod
def Name(cls):
return 'blob_storage.blob_storage'
@classmethod
def ValueCanBeAddedPredicate(cls, value, is_first_result):
if ('blob-writes' not in value.name and
'blob-reads' not in value.name):
return False
return value.values != None

@ -0,0 +1,169 @@
<script>
var blobs = [];
var bytes = 0;
var timeStart;
var timeEnd;
var doneReading = false;
var errors = [];
var disableUI = false;
function recordError(error) {
console.log(error);
errors.push(error);
}
function updateStats() {
if (disableUI) return;
var num_blobs = document.getElementById('num_blobs');
var total_bytes = document.getElementById('total_bytes');
var time = document.getElementById('time');
num_blobs.innerHTML = '' + blobs.length;
total_bytes.innerHTML = '' + bytes;
time.innerHTML = '' + (timeEnd - timeStart);
}
function createAndRead(size) {
doneReading = false;
errors = [];
var reader = new FileReader();
var currentBlob = 0;
var totalSize = 0;
var error = document.getElementById('error');
var numRead = 0;
var blob = new Blob([new Uint8Array(size)], {type: 'application/octet-string'});
reader.onloadend = function(e) {
if (reader.error) {
recordError('Error when reading blob: ' + reader.error);
doneReading = true;
return;
}
if (reader.result.byteLength != size) {
recordError("Sizes don't match");
}
doneReading = true;
}
reader.readAsArrayBuffer(blob);
}
function createBlob(size) {
timeStart = performance.now();
var blob = new Blob([new Uint8Array(size)], {type: 'application/octet-string'});
timeEnd = performance.now();
blobs.push(blob);
bytes += size;
updateStats();
}
function garbageCollect() {
blobs = [];
bytes = 0;
updateStats();
}
function addCustom() {
var custom_input = document.getElementById('custom_input');
var custom_bytes = custom_input.value;
createBlob(custom_bytes * 1);
}
function readBlobsSerially() {
doneReading = false;
errors = [];
if (blobs.length == 0) {
return;
}
timeStart = performance.now();
var reader = new FileReader();
var currentBlob = 0;
var totalSize = 0;
var error = document.getElementById('error');
var numRead = 0;
reader.onloadend = function(e) {
if (reader.error) {
if (!disableUI)
error.innerHTML += '<br/>Reader error:<br/>' + reader.error.message;
recordError('Error when reading blob ' + currentBlob + ': ' + reader.error);
doneReading = true;
return;
}
totalSize += reader.result.byteLength;
currentBlob++;
if (currentBlob < blobs.length) {
reader.readAsArrayBuffer(blobs[currentBlob]);
} else {
timeEnd = performance.now();
// we're done reading
if (totalSize != bytes) {
recordError("Sizes don't match");
if (!disableUI)
error.innerHTML += '<br/>Sizes don\'t match: ' + totalSize + ' vs ' + bytes;
}
doneReading = true;
updateStats();
}
}
reader.readAsArrayBuffer(blobs[currentBlob]);
}
function readBlobsInParallel() {
doneReading = false;
errors = [];
if (blobs.length == 0) {
return;
}
timeStart = performance.now();
var currentBlob = 0;
var totalSize = 0;
var error = document.getElementById('error');
var numRead = 0;
for (; currentBlob < blobs.length; currentBlob++) {
var genReader = function(index) {
var reader = new FileReader();
reader.onloadend = function(e) {
if (reader.error) {
if (!disableUI)
error.innerHTML += '<br/>Reader error:<br/>' + reader.error.message;
recordError('Error when reading blob ' + index + ': ' + reader.error);
doneReading = true;
return;
}
totalSize += reader.result.byteLength;
numRead++;
if (numRead >= blobs.length) {
timeEnd = performance.now();
// we're done reading
if (totalSize != bytes) {
recordError("Sizes don't match");
if (!disableUI)
error.innerHTML += '<br/>Sizes don\'t match: ' + totalSize + ' vs ' + bytes;
}
doneReading = true;
updateStats();
}
}
return reader;
}
genReader(currentBlob).readAsArrayBuffer(blobs[currentBlob]);
}
}
</script>
<div>Number of blobs: <span id="num_blobs">0</span>, total memory size: <span id="total_bytes">0</span> bytes.</div>
<input type="button" id="custom_input_submit" value="Add Blob with Size" onclick="addCustom();"/>
<input type="text" inputmode="numeric" id="custom_input" value=""/><br/>
Shorcut buttons for person:
<input type="button" value="2 bytes" onclick="createBlob(2);" />
<input type="button" value="1 kb" onclick="createBlob(1024);" />
<input type="button" value="16 kb" onclick="createBlob(16*1024);" />
<input type="button" value="1 mb" onclick="createBlob(1024 * 1024);" />
<input type="button" value="40 mb" onclick="createBlob(40 * 1024 * 1024);" />
<input type="button" value="400 mb" onclick="createBlob(400 * 1024 * 1024);" />
<input type="button" id="read_blobs_serial" value="Read Blobs Serially" onclick="readBlobsSerially();" />
<input type="button" id="read_blobs_parallel" value="Read Blobs Parallel" onclick="readBlobsInParallel();" /><br/>
<input type="button" id="garbage_collect" value="Garbage Collect Blobs" onclick="garbageCollect();" /><br/>
Operation Time:
<div id="time"></div>
Errors:
<div id="error"></div>

@ -0,0 +1,94 @@
# Copyright 2015 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.
from telemetry.page import page as page_module
from telemetry.page import page_test as page_test
from telemetry.page import page_set as page_set_module
NUM_BLOB_MASS_CREATE_READS = 15
class BlobCreateThenRead(page_module.Page):
def __init__(self, write_method, blob_sizes, page_set):
super(BlobCreateThenRead, self).__init__(
url='file://blob/blob-workshop.html',
page_set=page_set,
name='blob-create-read-' + write_method)
self._blob_sizes = blob_sizes
def RunPageInteractions(self, action_runner):
action_runner.ExecuteJavaScript('disableUI = true;')
for size_bytes in self._blob_sizes:
with action_runner.CreateInteraction('Action_CreateAndReadBlob',
repeatable=True):
action_runner.ExecuteJavaScript(
'createAndRead(' + str(size_bytes) + ');')
action_runner.WaitForJavaScriptCondition(
'doneReading === true || errors', 60)
errors = action_runner.EvaluateJavaScript('errors')
if errors:
raise page_test.Failure('Errors on page: ' + ', '.join(self.errors))
class BlobMassCreate(page_module.Page):
def __init__(self, write_method, blob_sizes, page_set):
super(BlobMassCreate, self).__init__(
url='file://blob/blob-workshop.html',
page_set=page_set,
name='blob-mass-create-' + write_method)
self._blob_sizes = blob_sizes
def RunPageInteractions(self, action_runner):
action_runner.ExecuteJavaScript('disableUI = true;')
# Add blobs
for size_bytes in self._blob_sizes:
with action_runner.CreateInteraction('Action_CreateBlob',
repeatable=True):
action_runner.ExecuteJavaScript('createBlob(' + str(size_bytes) + ');')
# Read blobs
for _ in range(0, NUM_BLOB_MASS_CREATE_READS):
with action_runner.CreateInteraction('Action_ReadBlobs',
repeatable=True):
action_runner.ExecuteJavaScript('readBlobsSerially();')
action_runner.WaitForJavaScriptCondition(
'doneReading === true || errors', 60)
errors = action_runner.EvaluateJavaScript('errors')
if errors:
raise page_test.Failure('Errors on page: ' + ', '.join(self.errors))
class BlobWorkshopPageSet(page_set_module.PageSet):
"""The BlobWorkshop page set."""
def __init__(self):
super(BlobWorkshopPageSet, self).__init__()
self.AddUserStory(
BlobMassCreate('2Bx200', [2] * 200, self))
self.AddUserStory(
BlobMassCreate('1KBx200', [1024] * 200, self))
self.AddUserStory(
BlobMassCreate('150KBx200', [150 * 1024] * 200, self))
self.AddUserStory(
BlobMassCreate('1MBx200', [1024 * 1024] * 200, self))
self.AddUserStory(
BlobMassCreate('10MBx30', [10 * 1024 * 1024] * 30, self))
self.AddUserStory(
BlobMassCreate('100MBx5', [100 * 1024 * 1024] * 5, self))
self.AddUserStory(BlobCreateThenRead('2Bx200', [2] * 200, self))
self.AddUserStory(BlobCreateThenRead('1KBx200', [1024] * 200, self))
self.AddUserStory(
BlobCreateThenRead('150KBx200', [150 * 1024 - 1] * 200, self))
self.AddUserStory(BlobCreateThenRead('1MBx200', [1024 * 1024] * 200, self))
self.AddUserStory(
BlobCreateThenRead('10MBx30', [10 * 1024 * 1024] * 30, self))
self.AddUserStory(
BlobCreateThenRead('100MBx5', [100 * 1024 * 1024] * 5, self))

@ -0,0 +1,112 @@
# Copyright 2015 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.
from telemetry.value import list_of_scalar_values
from telemetry.web_perf.metrics import timeline_based_metric
WRITE_EVENT_NAME = 'Registry::RegisterBlob'
READ_EVENT_NAME = 'BlobRequest'
class BlobTimelineMetric(timeline_based_metric.TimelineBasedMetric):
"""BlobTimelineMetric reports timing information about blob storage.
The following metrics are added to the results:
* blob write times (blob_writes)
* blob read times (blob_reads)
"""
def __init__(self):
super(BlobTimelineMetric, self).__init__()
@staticmethod
def IsWriteEvent(event):
return event.name == WRITE_EVENT_NAME
@staticmethod
def IsReadEvent(event):
return event.name == READ_EVENT_NAME
@staticmethod
def IsEventInInteraction(event, interaction):
return interaction.start <= event.start <= interaction.end
@staticmethod
def ThreadDurationIfPresent(event):
if event.thread_duration:
return event.thread_duration
else:
return event.end - event.start
def AddResults(self, model, renderer_thread, interactions, results):
assert interactions
browser_process = [p for p in model.GetAllProcesses()
if p.name == "Browser"][0]
write_events = []
read_events = []
for event in renderer_thread.parent.IterAllEvents(
event_predicate=self.IsWriteEvent):
write_events.append(event)
for event in browser_process.parent.IterAllEvents(
event_predicate=self.IsReadEvent):
read_events.append(event)
# Only these private methods are tested for mocking simplicity.
self._AddWriteResultsInternal(write_events, interactions, results)
self._AddReadResultsInternal(read_events, interactions, results)
def _AddWriteResultsInternal(self, events, interactions, results):
writes = []
for event in events:
if (self.IsWriteEvent(event) and
any(self.IsEventInInteraction(event, interaction)
for interaction in interactions)):
writes.append(self.ThreadDurationIfPresent(event))
if writes:
results.AddValue(list_of_scalar_values.ListOfScalarValues(
page=results.current_page,
name='blob-writes',
units='ms',
values=writes,
description='List of durations of blob writes.'))
else:
results.AddValue(list_of_scalar_values.ListOfScalarValues(
page=results.current_page,
name='blob-writes',
units='ms',
values=None,
none_value_reason='No blob write events found for this interaction.'))
def _AddReadResultsInternal(self, events, interactions, results):
reads = dict()
for event in events:
if (not self.IsReadEvent(event) or
not any(self.IsEventInInteraction(event, interaction)
for interaction in interactions)):
continue
# Every blob has unique UUID. To get the total time for reading
# a blob, we add up the time of all events with the same blob UUID.
uuid = event.args['uuid']
if uuid not in reads:
reads[uuid] = 0
reads[uuid] += self.ThreadDurationIfPresent(event)
if reads:
results.AddValue(list_of_scalar_values.ListOfScalarValues(
page=results.current_page,
name='blob-reads',
units='ms',
values=reads.values(),
description='List of read times for blobs.'))
else:
results.AddValue(list_of_scalar_values.ListOfScalarValues(
page=results.current_page,
name='blob-reads',
units='ms',
values=None,
none_value_reason='No blob read events found for this interaction.'))

@ -0,0 +1,139 @@
# Copyright 2015 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.
import unittest
from collections import namedtuple
from telemetry.page import page
from telemetry.results import page_test_results
from telemetry.web_perf.metrics import blob_timeline
from telemetry.web_perf import timeline_interaction_record
FakeEvent = namedtuple('Event', 'name, start, end, thread_duration, args')
Interaction = timeline_interaction_record.TimelineInteractionRecord
TEST_INTERACTION_LABEL = 'Action_TestInteraction'
WRITE_EVENT_NAME = 'Registry::RegisterBlob'
READ_EVENT_NAME = 'BlobRequest'
def GetBlobMetrics(events, interactions):
results = page_test_results.PageTestResults()
test_page = page.Page('file://blank.html')
results.WillRunPage(test_page)
blob_timeline.BlobTimelineMetric()._AddWriteResultsInternal(
events, interactions, results) # pylint:disable=protected-access
blob_timeline.BlobTimelineMetric()._AddReadResultsInternal(
events, interactions, results) # pylint:disable=protected-access
return_dict = dict((value.name, value.values) for value in
results.current_page_run.values)
results.DidRunPage(test_page)
return return_dict
def FakeWriteEvent(start, end, thread_duration=None):
if not thread_duration:
thread_duration = end - start
return FakeEvent(blob_timeline.WRITE_EVENT_NAME,
start, end, thread_duration, {'uuid':'fakeuuid'})
def FakeReadEvent(start, end, uuid, thread_duration=None):
if not thread_duration:
thread_duration = end - start
return FakeEvent(blob_timeline.READ_EVENT_NAME,
start, end, thread_duration, {'uuid': uuid})
def TestInteraction(start, end):
return Interaction(TEST_INTERACTION_LABEL, start, end)
class BlobTimelineMetricUnitTest(unittest.TestCase):
def testWriteMetric(self):
events = [FakeWriteEvent(0, 1),
FakeWriteEvent(9, 11),
FakeWriteEvent(10, 13),
FakeWriteEvent(20, 24),
FakeWriteEvent(21, 26),
FakeWriteEvent(29, 35),
FakeWriteEvent(30, 37),
FakeWriteEvent(40, 48),
FakeWriteEvent(41, 50),
FakeEvent('something', 10, 13, 3, {}),
FakeEvent('FrameView::something', 20, 24, 4, {}),
FakeEvent('SomeThing::performLayout', 30, 37, 7, {}),
FakeEvent('something else', 40, 48, 8, {})]
interactions = [TestInteraction(10, 20),
TestInteraction(30, 40)]
self.assertEqual({'blob-reads': None, 'blob-writes': None},
GetBlobMetrics(events, []))
self.assertEqual({'blob-reads': None, 'blob-writes': None},
GetBlobMetrics([], interactions))
# The first event starts before the first interaction, so it is ignored.
# The second event starts before the first interaction, so it is ignored.
# The third event starts during the first interaction, and its duration is
# 13 - 10 = 3.
# The fourth event starts during the first interaction, and its duration is
# 24 - 20 = 4.
# The fifth event starts between the two interactions, so it is ignored.
# The sixth event starts between the two interactions, so it is ignored.
# The seventh event starts during the second interaction, and its duration
# is 37 - 30 = 7.
# The eighth event starts during the second interaction and its duration is
# 48 - 40 = 8.
# The ninth event starts after the last interaction, so it is ignored.
# The rest of the events are not layout events, so they are ignored.
self.assertEqual({'blob-reads': None, 'blob-writes': [3, 4, 7, 8]},
GetBlobMetrics(events, interactions))
def testReadMetric(self):
events = [FakeReadEvent(0, 1, 'a'),
FakeReadEvent(9, 11, 'a'),
FakeReadEvent(10, 13, 'b', 1), # counts
FakeReadEvent(15, 18, 'b'), # counts
FakeReadEvent(21, 26, 'b'),
FakeReadEvent(29, 35, 'c'),
FakeReadEvent(31, 32, 'e'), # counts
FakeReadEvent(34, 36, 'e', 1), # counts
FakeReadEvent(32, 37, 'd'), # counts
FakeEvent('something', 10, 13, 3, {}),
FakeEvent('something else', 40, 48, 8, {})]
interactions = [TestInteraction(10, 20),
TestInteraction(30, 40)]
self.assertEqual({'blob-reads': None, 'blob-writes': None},
GetBlobMetrics(events, []))
self.assertEqual({'blob-reads': None, 'blob-writes': None},
GetBlobMetrics([], interactions))
# We ignore events outside of the interaction intervals, and we use the
# begining of the first event of the interval and the end of the last
# event.
# 18 - 10 = 8
# 37 - 32 = 5
self.assertEqual({'blob-reads': [4, 2, 5], 'blob-writes': None},
GetBlobMetrics(events, interactions))
def testReadAndWriteMetrics(self):
events = [FakeReadEvent(0, 1, 'a'),
FakeReadEvent(9, 11, 'a'),
FakeReadEvent(10, 13, 'b'), # counts
FakeWriteEvent(15, 18), # counts
FakeReadEvent(21, 26, 'c'),
FakeReadEvent(29, 35, 'd'),
FakeWriteEvent(31, 34, 1), # counts
FakeReadEvent(32, 33, 'e'), # counts
FakeReadEvent(34, 35, 'e'), # counts
FakeEvent('something', 31, 33, 2, {})]
interactions = [TestInteraction(10, 20),
TestInteraction(30, 35)]
self.assertEqual({'blob-reads': None, 'blob-writes': None},
GetBlobMetrics(events, []))
self.assertEqual({'blob-reads': None, 'blob-writes': None},
GetBlobMetrics([], interactions))
# We use the read events in the interactions, so the same as the test above.
self.assertEqual({'blob-reads': [3, 2], 'blob-writes': [3, 1]},
GetBlobMetrics(events, interactions))

@ -8,6 +8,7 @@ from telemetry.core.platform import tracing_category_filter
from telemetry.core.platform import tracing_options
from telemetry.timeline import model as model_module
from telemetry.value import trace
from telemetry.web_perf.metrics import blob_timeline
from telemetry.web_perf.metrics import gpu_timeline
from telemetry.web_perf.metrics import layout
from telemetry.web_perf.metrics import responsiveness_metric
@ -37,7 +38,8 @@ def _GetAllTimelineBasedMetrics():
return (smoothness.SmoothnessMetric(),
responsiveness_metric.ResponsivenessMetric(),
layout.LayoutMetric(),
gpu_timeline.GPUTimelineMetric())
gpu_timeline.GPUTimelineMetric(),
blob_timeline.BlobTimelineMetric())
class InvalidInteractions(Exception):