diff options
author | szym@chromium.org <szym@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-01-22 17:49:55 +0000 |
---|---|---|
committer | szym@chromium.org <szym@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-01-22 17:49:55 +0000 |
commit | 90f69904f78bd1584d8f36a140cb2f4a728cc39d (patch) | |
tree | 8e74b331db5d71dfcce84e921a66e9575cdbd0e6 | |
parent | 62f989801281c55025514a69b606b95e6ea48aa0 (diff) | |
download | chromium_src-90f69904f78bd1584d8f36a140cb2f4a728cc39d.zip chromium_src-90f69904f78bd1584d8f36a140cb2f4a728cc39d.tar.gz chromium_src-90f69904f78bd1584d8f36a140cb2f4a728cc39d.tar.bz2 |
[telemetry] bitmaptools as a standalone executable
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
Original review: https://codereview.chromium.org/136793022
TBR=tonyg,maruel
Review URL: https://codereview.chromium.org/131563009
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@246361 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | PRESUBMIT.py | 2 | ||||
-rw-r--r-- | build/all.gyp | 5 | ||||
-rw-r--r-- | build/all_android.gyp | 1 | ||||
-rw-r--r-- | chrome/telemetry.isolate | 1 | ||||
-rw-r--r-- | tools/android/android_tools.gyp | 1 | ||||
-rw-r--r-- | tools/telemetry/telemetry.gyp | 16 | ||||
-rw-r--r-- | tools/telemetry/telemetry/core/bitmap.py | 140 | ||||
-rw-r--r-- | tools/telemetry/telemetry/core/bitmap_unittest.py | 12 | ||||
-rw-r--r-- | tools/telemetry/telemetry/core/bitmaptools.cc | 264 |
9 files changed, 391 insertions, 51 deletions
diff --git a/PRESUBMIT.py b/PRESUBMIT.py index 9407609..8f024e9 100644 --- a/PRESUBMIT.py +++ b/PRESUBMIT.py @@ -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) diff --git a/build/all.gyp b/build/all.gyp index 22d8615..1bf93d72 100644 --- a/build/all.gyp +++ b/build/all.gyp @@ -69,6 +69,7 @@ '../third_party/qcms/qcms.gyp:*', '../tools/gn/gn.gyp:*', '../tools/perf/clear_system_cache/clear_system_cache.gyp:*', + '../tools/telemetry/telemetry.gyp:*', '../v8/tools/gyp/v8.gyp:*', '../webkit/glue/webkit_glue.gyp:*', '../webkit/renderer/compositor_bindings/compositor_bindings_tests.gyp:*', @@ -265,6 +266,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', ], }], @@ -436,6 +438,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"', { @@ -472,6 +475,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"', { @@ -506,6 +510,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"', { diff --git a/build/all_android.gyp b/build/all_android.gyp index aa58d65..e60c14e 100644 --- a/build/all_android.gyp +++ b/build/all_android.gyp @@ -28,6 +28,7 @@ # the sync-related code for Android has been upstreamed. # See http://crbug.com/159203 '../third_party/cacheinvalidation/cacheinvalidation.gyp:cacheinvalidation_javalib', + '../tools/telemetry/telemetry.gyp:*#host', ], }, # target_name: All { diff --git a/chrome/telemetry.isolate b/chrome/telemetry.isolate index 7a98160..1c09db8 100644 --- a/chrome/telemetry.isolate +++ b/chrome/telemetry.isolate @@ -25,6 +25,7 @@ '../third_party/WebKit/PerformanceTests/resources/statistics.js', '../third_party/flot/jquery.flot.min.js', '../tools/perf/unit-info.json', + '<(PRODUCT_DIR)/bitmaptools<(EXECUTABLE_SUFFIX)', ], 'isolate_dependency_untracked': [ '../build/android/pylib/', diff --git a/tools/android/android_tools.gyp b/tools/android/android_tools.gyp index ed5e905..b780428 100644 --- a/tools/android/android_tools.gyp +++ b/tools/android/android_tools.gyp @@ -14,6 +14,7 @@ 'forwarder2/forwarder.gyp:forwarder2', 'md5sum/md5sum.gyp:md5sum', 'purge_ashmem/purge_ashmem.gyp:purge_ashmem', + '../../tools/telemetry/telemetry.gyp:*#host', ], }, { diff --git a/tools/telemetry/telemetry.gyp b/tools/telemetry/telemetry.gyp new file mode 100644 index 0000000..150f76c --- /dev/null +++ b/tools/telemetry/telemetry.gyp @@ -0,0 +1,16 @@ +# 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', + ], + 'toolsets': ['host'], + }, + ], +} diff --git a/tools/telemetry/telemetry/core/bitmap.py b/tools/telemetry/telemetry/core/bitmap.py index 8c52d3b..9b4c163 100644 --- a/tools/telemetry/telemetry/core/bitmap.py +++ b/tools/telemetry/telemetry/core/bitmap.py @@ -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 @@ -43,6 +53,62 @@ WEB_PAGE_TEST_ORANGE = RgbaColor(222, 100, 13) 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.""" @@ -58,6 +124,7 @@ class Bitmap(object): self._height = height self._pixels = pixels self._metadata = metadata or {} + self._crop_box = None @property def bpp(self): @@ -67,16 +134,27 @@ class Bitmap(object): @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 @@ -90,12 +168,13 @@ class Bitmap(object): 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: @@ -179,49 +258,19 @@ class Bitmap(object): """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): @@ -234,5 +283,4 @@ class Bitmap(object): 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) diff --git a/tools/telemetry/telemetry/core/bitmap_unittest.py b/tools/telemetry/telemetry/core/bitmap_unittest.py index b0b01f5..095cc9a 100644 --- a/tools/telemetry/telemetry/core/bitmap_unittest.py +++ b/tools/telemetry/telemetry/core/bitmap_unittest.py @@ -8,7 +8,7 @@ import unittest 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. @@ -57,6 +57,7 @@ class BitmapTest(unittest.TestCase): 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] @@ -102,7 +103,7 @@ class BitmapTest(unittest.TestCase): 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, @@ -116,6 +117,7 @@ class BitmapTest(unittest.TestCase): 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, @@ -129,7 +131,7 @@ class BitmapTest(unittest.TestCase): 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, @@ -148,7 +150,7 @@ class BitmapTest(unittest.TestCase): 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, @@ -165,7 +167,7 @@ class BitmapTest(unittest.TestCase): 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] diff --git a/tools/telemetry/telemetry/core/bitmaptools.cc b/tools/telemetry/telemetry/core/bitmaptools.cc new file mode 100644 index 0000000..ba4a8a5 --- /dev/null +++ b/tools/telemetry/telemetry/core/bitmaptools.cc @@ -0,0 +1,264 @@ +// 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. + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#if defined(WIN32) +#include <fcntl.h> +#include <io.h> +#endif + +enum Commands { + CROP_PIXELS = 0, + HISTOGRAM = 1, + BOUNDING_BOX = 2 +}; + +bool ReadInt(int* out) { + return fread(out, sizeof(*out), 1, stdin) == 1; +} + +void WriteResponse(void* data, int size) { + fwrite(&size, sizeof(size), 1, stdout); + fwrite(data, size, 1, stdout); + fflush(stdout); +} + +struct Box { + Box() : left(), top(), right(), bottom() {} + + // Expected input is: + // left, top, width, height + bool Read() { + int width; + int height; + if (!(ReadInt(&left) && ReadInt(&top) && + ReadInt(&width) && ReadInt(&height))) { + fprintf(stderr, "Could not parse Box.\n"); + return false; + } + if (left < 0 || top < 0 || width < 0 || height < 0) { + fprintf(stderr, "Box dimensions must be non-negative.\n"); + return false; + } + right = left + width; + bottom = top + height; + return true; + } + + void Union(int x, int y) { + if (left > x) left = x; + if (right <= x) right = x + 1; + if (top > y) top = y; + if (bottom <= y) bottom = y + 1; + } + + int width() const { return right - left; } + int height() const { return bottom - top; } + + int left; + int top; + int right; + int bottom; +}; + + +// Represents a bitmap buffer with a crop box. +struct Bitmap { + Bitmap() : pixels(NULL) {} + + ~Bitmap() { + if (pixels) + delete[] pixels; + } + + // Expected input is: + // bpp, width, height, box, pixels + bool Read() { + int bpp; + int width; + int height; + if (!(ReadInt(&bpp) && ReadInt(&width) && ReadInt(&height))) { + fprintf(stderr, "Could not parse Bitmap initializer.\n"); + return false; + } + if (bpp <= 0 || width <= 0 || height <= 0) { + fprintf(stderr, "Dimensions must be positive.\n"); + return false; + } + + int size = width * height * bpp; + + row_stride = width * bpp; + pixel_stride = bpp; + total_size = size; + row_size = row_stride; + + if (!box.Read()) { + fprintf(stderr, "Expected crop box argument not found.\n"); + return false; + } + + if (box.bottom * row_stride > total_size || + box.right * pixel_stride > row_size) { + fprintf(stderr, "Crop box overflows the bitmap.\n"); + return false; + } + + pixels = new unsigned char[size]; + if (fread(pixels, sizeof(pixels[0]), size, stdin) < + static_cast<size_t>(size)) { + fprintf(stderr, "Not enough pixels found,\n"); + return false; + } + + total_size = (box.bottom - box.top) * row_stride; + row_size = (box.right - box.left) * pixel_stride; + data = pixels + box.top * row_stride + box.left * pixel_stride; + return true; + } + + void WriteCroppedPixels() const { + int out_size = row_size * box.height(); + unsigned char* out = new unsigned char[out_size]; + unsigned char* dst = out; + for (const unsigned char* row = data; + row < data + total_size; + row += row_stride, dst += row_size) { + // No change in pixel_stride, so we can copy whole rows. + memcpy(dst, row, row_size); + } + + WriteResponse(out, out_size); + delete[] out; + } + + unsigned char* pixels; + Box box; + // Points at the top-left pixel in |pixels|. + const unsigned char* data; + // These counts are in bytes. + int row_stride; + int pixel_stride; + int total_size; + int row_size; +}; + + +static inline +bool PixelsEqual(const unsigned char* pixel1, const unsigned char* pixel2, + int tolerance) { + // Note: this works for both RGB and RGBA. Alpha channel is ignored. + return (abs(pixel1[0] - pixel2[0]) <= tolerance) && + (abs(pixel1[1] - pixel2[1]) <= tolerance) && + (abs(pixel1[2] - pixel2[2]) <= tolerance); +} + + +static inline +bool PixelsEqual(const unsigned char* pixel, int color, int tolerance) { + unsigned char pixel2[3] = { color >> 16, color >> 8, color }; + return PixelsEqual(pixel, pixel2, tolerance); +} + + +static +bool Histogram(const Bitmap& bmp) { + int ignore_color; + int tolerance; + if (!(ReadInt(&ignore_color) && ReadInt(&tolerance))) { + fprintf(stderr, "Could not parse HISTOGRAM command.\n"); + return false; + } + + const int kLength = 3 * 256; + int counts[kLength] = {}; + + for (const unsigned char* row = bmp.data; row < bmp.data + bmp.total_size; + row += bmp.row_stride) { + for (const unsigned char* pixel = row; pixel < row + bmp.row_size; + pixel += bmp.pixel_stride) { + if (ignore_color >= 0 && PixelsEqual(pixel, ignore_color, tolerance)) + continue; + ++(counts[256 * 0 + pixel[0]]); + ++(counts[256 * 1 + pixel[1]]); + ++(counts[256 * 2 + pixel[2]]); + } + } + + WriteResponse(counts, sizeof(counts)); + return true; +} + + +static +bool BoundingBox(const Bitmap& bmp) { + int color; + int tolerance; + if (!(ReadInt(&color) && ReadInt(&tolerance))) { + fprintf(stderr, "Could not parse BOUNDING_BOX command.\n"); + return false; + } + + Box box; + box.left = bmp.total_size; + box.top = bmp.total_size; + box.right = 0; + box.bottom = 0; + + int count = 0; + int y = 0; + for (const unsigned char* row = bmp.data; row < bmp.data + bmp.total_size; + row += bmp.row_stride, ++y) { + int x = 0; + for (const unsigned char* pixel = row; pixel < row + bmp.row_size; + pixel += bmp.pixel_stride, ++x) { + if (!PixelsEqual(pixel, color, tolerance)) + continue; + box.Union(x, y); + ++count; + } + } + + int response[] = { box.left, box.top, box.width(), box.height(), count }; + WriteResponse(response, sizeof(response)); + return true; +} + + +int main() { + Bitmap bmp; + int command; + +#if defined(WIN32) + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +#else + FILE* unused_stdin = freopen(NULL, "rb", stdin); + FILE* unused_stdout = freopen(NULL, "wb", stdout); +#endif + + if (!bmp.Read()) return -1; + if (!ReadInt(&command)) { + fprintf(stderr, "Expected command.\n"); + return -1; + } + switch (command) { + case CROP_PIXELS: + bmp.WriteCroppedPixels(); + break; + case BOUNDING_BOX: + if (!BoundingBox(bmp)) return -1; + break; + case HISTOGRAM: + if (!Histogram(bmp)) return -1; + break; + default: + fprintf(stderr, "Unrecognized command\n"); + return -1; + } + return 0; +} |