Skip to content

Commit

Permalink
[telemetry] bitmaptools as a standalone executable
Browse files Browse the repository at this point in the history
The C++ binary implements simple per-pixel algorithms for SpeedIndex
computation. This allows us to achieve near real-time processing without
bringing external dependencies.

The bitmaptools binary needs to be built before it can be used.
The overhead of spawning a child process for each frame is about 3ms.

BUG=323813
TEST=telemetry bitmap_unittest

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

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@245684 0039d316-1c4b-4281-b951-d872f2087c98
  • Loading branch information
szym@chromium.org committed Jan 18, 2014
1 parent d3ee6a9 commit 1d7d185
Show file tree
Hide file tree
Showing 6 changed files with 386 additions and 51 deletions.
2 changes: 2 additions & 0 deletions PRESUBMIT.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,8 @@ def _CheckSpamLogging(input_api, output_api):
r"^remoting[\\\/]base[\\\/]logging\.h$",
r"^remoting[\\\/]host[\\\/].*",
r"^sandbox[\\\/]linux[\\\/].*",
r"^tools[\\\/]telemetry[\\\/]telemetry[\\\/]core[\\\/]"
r"bitmaptools.cc$",
r"^ui[\\\/]aura[\\\/]bench[\\\/]bench_main\.cc$",))
source_file_filter = lambda x: input_api.FilterSourceFile(
x, white_list=(file_inclusion_pattern,), black_list=black_list)
Expand Down
4 changes: 4 additions & 0 deletions build/all.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@
'../third_party/cacheinvalidation/cacheinvalidation.gyp:cacheinvalidation_unittests',
'../third_party/libaddressinput/libaddressinput.gyp:libaddressinput_unittests',
'../third_party/libphonenumber/libphonenumber.gyp:libphonenumber_unittests',
'../tools/telemetry/telemetry.gyp:*',
'../webkit/renderer/compositor_bindings/compositor_bindings_tests.gyp:webkit_compositor_bindings_unittests',
],
}],
Expand Down Expand Up @@ -436,6 +437,7 @@
'../chrome/chrome.gyp:sync_performance_tests',
'../media/media.gyp:media_perftests',
'../tools/perf/clear_system_cache/clear_system_cache.gyp:*',
'../tools/telemetry/telemetry.gyp:*',
],
'conditions': [
['OS!="ios" and OS!="win"', {
Expand Down Expand Up @@ -472,6 +474,7 @@
'../gpu/gles2_conform_support/gles2_conform_test.gyp:gles2_conform_test',
'../gpu/gpu.gyp:gl_tests',
'../gpu/gpu.gyp:angle_unittests',
'../tools/telemetry/telemetry.gyp:*',
],
'conditions': [
['OS!="ios" and OS!="win"', {
Expand Down Expand Up @@ -506,6 +509,7 @@
'../gpu/gles2_conform_support/gles2_conform_test.gyp:gles2_conform_test',
'../gpu/gpu.gyp:gl_tests',
'../gpu/gpu.gyp:angle_unittests',
'../tools/telemetry/telemetry.gyp:*',
],
'conditions': [
['OS!="ios" and OS!="win"', {
Expand Down
15 changes: 15 additions & 0 deletions tools/telemetry/telemetry.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2014 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.

{
'targets': [
{
'target_name': 'bitmaptools',
'type': 'executable',
'sources': [
'telemetry/core/bitmaptools.cc',
],
},
],
}
140 changes: 94 additions & 46 deletions tools/telemetry/telemetry/core/bitmap.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
# Copyright 2013 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.

"""
Bitmap is a basic wrapper for image pixels. It includes some basic processing
tools: crop, find bounding box of a color and compute histogram of color values.
"""

import array
import base64
import cStringIO
import struct
import subprocess
import sys

from telemetry.core import util

Expand Down Expand Up @@ -43,6 +53,62 @@ def AssertIsRGBA(self, r, g, b, a, tolerance=0):
WHITE = RgbaColor(255, 255, 255)


class _BitmapTools(object):
"""Wraps a child process of bitmaptools and allows for one command."""
CROP_PIXELS = 0
HISTOGRAM = 1
BOUNDING_BOX = 2

def __init__(self, dimensions, pixels):
suffix = '.exe' if sys.platform == 'win32' else ''
binary = util.FindSupportBinary('bitmaptools' + suffix)
assert binary, 'You must build bitmaptools first!'

self._popen = subprocess.Popen([binary],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)

# dimensions are: bpp, width, height, boxleft, boxtop, boxwidth, boxheight
packed_dims = struct.pack('iiiiiii', *dimensions)
self._popen.stdin.write(packed_dims)
# If we got a list of ints, we need to convert it into a byte buffer.
if type(pixels) is not bytearray:
pixels = bytearray(pixels)
self._popen.stdin.write(pixels)

def _RunCommand(self, *command):
assert not self._popen.stdin.closed, (
'Exactly one command allowed per instance of tools.')
packed_command = struct.pack('i' * len(command), *command)
self._popen.stdin.write(packed_command)
self._popen.stdin.close()
length_packed = self._popen.stdout.read(struct.calcsize('i'))
if not length_packed:
raise Exception(self._popen.stderr.read())
length = struct.unpack('i', length_packed)[0]
return self._popen.stdout.read(length)

def CropPixels(self):
return self._RunCommand(_BitmapTools.CROP_PIXELS)

def Histogram(self, ignore_color, tolerance):
ignore_color = -1 if ignore_color is None else int(ignore_color)
response = self._RunCommand(_BitmapTools.HISTOGRAM, ignore_color, tolerance)
out = array.array('i')
out.fromstring(response)
return out

def BoundingBox(self, color, tolerance):
response = self._RunCommand(_BitmapTools.BOUNDING_BOX, int(color),
tolerance)
unpacked = struct.unpack('iiiii', response)
box, count = unpacked[:4], unpacked[-1]
if box[2] < 0 or box[3] < 0:
box = None
return box, count


class Bitmap(object):
"""Utilities for parsing and inspecting a bitmap."""

Expand All @@ -58,6 +124,7 @@ def __init__(self, bpp, width, height, pixels, metadata=None):
self._height = height
self._pixels = pixels
self._metadata = metadata or {}
self._crop_box = None

@property
def bpp(self):
Expand All @@ -67,16 +134,27 @@ def bpp(self):
@property
def width(self):
"""Width of the bitmap."""
return self._width
return self._crop_box[2] if self._crop_box else self._width

@property
def height(self):
"""Height of the bitmap."""
return self._height
return self._crop_box[3] if self._crop_box else self._height

def _PrepareTools(self):
"""Prepares an instance of _BitmapTools which allows exactly one command.
"""
crop_box = self._crop_box or (0, 0, self._width, self._height)
return _BitmapTools((self._bpp, self._width, self._height) + crop_box,
self._pixels)

@property
def pixels(self):
"""Flat pixel array of the bitmap."""
if self._crop_box:
self._pixels = self._PrepareTools().CropPixels()
_, _, self._width, self._height = self._crop_box
self._crop_box = None
if type(self._pixels) is not bytearray:
self._pixels = bytearray(self._pixels)
return self._pixels
Expand All @@ -90,12 +168,13 @@ def metadata(self):

def GetPixelColor(self, x, y):
"""Returns a RgbaColor for the pixel at (x, y)."""
pixels = self.pixels
base = self._bpp * (y * self._width + x)
if self._bpp == 4:
return RgbaColor(self._pixels[base + 0], self._pixels[base + 1],
self._pixels[base + 2], self._pixels[base + 3])
return RgbaColor(self._pixels[base + 0], self._pixels[base + 1],
self._pixels[base + 2])
return RgbaColor(pixels[base + 0], pixels[base + 1],
pixels[base + 2], pixels[base + 3])
return RgbaColor(pixels[base + 0], pixels[base + 1],
pixels[base + 2])

def WritePngFile(self, path):
with open(path, "wb") as f:
Expand Down Expand Up @@ -179,49 +258,19 @@ def GetBoundingBox(self, color, tolerance=0):
"""Finds the minimum box surrounding all occurences of |color|.
Returns: (top, left, width, height), match_count
Ignores the alpha channel."""
# TODO(szym): Implement this.
raise NotImplementedError("GetBoundingBox not yet implemented.")
return self._PrepareTools().BoundingBox(color, tolerance)

def Crop(self, left, top, width, height):
"""Crops the current bitmap down to the specified box.
TODO(szym): Make this O(1).
"""
"""Crops the current bitmap down to the specified box."""
cur_box = self._crop_box or (0, 0, self._width, self._height)
cur_left, cur_top, cur_width, cur_height = cur_box

if (left < 0 or top < 0 or
(left + width) > self.width or
(top + height) > self.height):
(left + width) > cur_width or
(top + height) > cur_height):
raise ValueError('Invalid dimensions')

img_data = [[0 for x in xrange(width * self.bpp)]
for y in xrange(height)]

# Copy each pixel in the sub-rect.
# TODO(tonyg): Make this faster by avoiding the copy and artificially
# restricting the dimensions.
for y in range(height):
for x in range(width):
c = self.GetPixelColor(x + left, y + top)
offset = x * self.bpp
img_data[y][offset] = c.r
img_data[y][offset + 1] = c.g
img_data[y][offset + 2] = c.b
if self.bpp == 4:
img_data[y][offset + 3] = c.a

# This particular method can only save to a file, so the result will be
# written into an in-memory buffer and read back into a Bitmap
crop_img = png.from_array(img_data, mode='RGBA' if self.bpp == 4 else 'RGB')
output = cStringIO.StringIO()
try:
crop_img.save(output)
width, height, pixels, meta = png.Reader(
bytes=output.getvalue()).read_flat()
self._width = width
self._height = height
self._pixels = pixels
self._metadata = meta
finally:
output.close()

self._crop_box = cur_left + left, cur_top + top, width, height
return self

def ColorHistogram(self, ignore_color=None, tolerance=0):
Expand All @@ -234,5 +283,4 @@ def ColorHistogram(self, ignore_color=None, tolerance=0):
A list of 3x256 integers formatted as
[r0, r1, ..., g0, g1, ..., b0, b1, ...].
"""
# TODO(szym): Implement this.
raise NotImplementedError("ColorHistogram not yet implemented.")
return self._PrepareTools().Histogram(ignore_color, tolerance)
12 changes: 7 additions & 5 deletions tools/telemetry/telemetry/core/bitmap_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from telemetry.core import bitmap
from telemetry.core import util
from telemetry.unittest import DisabledTest
from telemetry.unittest import DisabledTestOnCrOS

# This is a simple base64 encoded 2x2 PNG which contains, in order, a single
# Red, Yellow, Blue, and Green pixel.
Expand Down Expand Up @@ -57,6 +57,7 @@ def testWritePngToPngFile(self):
new_file = bitmap.Bitmap.FromPngFile(temp_file)
self.assertTrue(orig.IsEqual(new_file))

@DisabledTestOnCrOS
def testWriteCroppedBmpToPngFile(self):
pixels = [255,0,0, 255,255,0, 0,0,0,
255,255,0, 0,255,0, 0,0,0]
Expand Down Expand Up @@ -102,7 +103,7 @@ def testDiff(self):
diff_bmp.GetPixelColor(2, 1).AssertIsRGB(255, 255, 255)
diff_bmp.GetPixelColor(2, 2).AssertIsRGB(255, 255, 255)

@DisabledTest
@DisabledTestOnCrOS
def testGetBoundingBox(self):
pixels = [0,0,0, 0,0,0, 0,0,0, 0,0,0,
0,0,0, 1,0,0, 1,0,0, 0,0,0,
Expand All @@ -116,6 +117,7 @@ def testGetBoundingBox(self):
self.assertEquals(box, None)
self.assertEquals(count, 0)

@DisabledTestOnCrOS
def testCrop(self):
pixels = [0,0,0, 1,0,0, 2,0,0, 3,0,0,
0,1,0, 1,1,0, 2,1,0, 3,1,0,
Expand All @@ -129,7 +131,7 @@ def testCrop(self):
bmp.GetPixelColor(1, 0).AssertIsRGB(2, 2, 0)
self.assertEquals(bmp.pixels, bytearray([1,2,0, 2,2,0]))

@DisabledTest
@DisabledTestOnCrOS
def testHistogram(self):
pixels = [1,2,3, 1,2,3, 1,2,3, 1,2,3,
1,2,3, 8,7,6, 5,4,6, 1,2,3,
Expand All @@ -148,7 +150,7 @@ def testHistogram(self):
self.assertEquals(histogram[3 + 512], 0)
self.assertEquals(histogram[6 + 512], 4)

@DisabledTest
@DisabledTestOnCrOS
def testHistogramIgnoreColor(self):
pixels = [1,2,3, 1,2,3, 1,2,3, 1,2,3,
1,2,3, 8,7,6, 5,4,6, 1,2,3,
Expand All @@ -165,7 +167,7 @@ def testHistogramIgnoreColor(self):
self.assertEquals(histogram[3 + 512], 0)
self.assertEquals(histogram[6 + 512], 4)

@DisabledTest
@DisabledTestOnCrOS
def testHistogramIgnoreColorTolerance(self):
pixels = [1,2,3, 4,5,6,
7,8,9, 8,7,6]
Expand Down
Loading

0 comments on commit 1d7d185

Please sign in to comment.