Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1104)

Side by Side Diff: gm/rebaseline_server/compare_rendered_pictures.py

Issue 856103002: Revert "Revert "delete old things!"" (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: Created 5 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 #!/usr/bin/python
2
3 """
4 Copyright 2014 Google Inc.
5
6 Use of this source code is governed by a BSD-style license that can be
7 found in the LICENSE file.
8
9 Compare results of two render_pictures runs.
10
11 TODO(epoger): Start using this module to compare ALL images (whether they
12 were generated from GMs or SKPs), and rename it accordingly.
13 """
14
15 # System-level imports
16 import logging
17 import os
18 import shutil
19 import subprocess
20 import tempfile
21 import time
22
23 # Must fix up PYTHONPATH before importing from within Skia
24 import rs_fixpypath # pylint: disable=W0611
25
26 # Imports from within Skia
27 from py.utils import git_utils
28 from py.utils import gs_utils
29 from py.utils import url_utils
30 import buildbot_globals
31 import column
32 import gm_json
33 import imagediffdb
34 import imagepair
35 import imagepairset
36 import results
37
38 # URL under which all render_pictures images can be found in Google Storage.
39 #
40 # TODO(epoger): In order to allow live-view of GMs and other images, read this
41 # from the input summary files, or allow the caller to set it within the
42 # GET_live_results call.
43 DEFAULT_IMAGE_BASE_GS_URL = 'gs://' + buildbot_globals.Get('skp_images_bucket')
44
45 # Column descriptors, and display preferences for them.
46 COLUMN__RESULT_TYPE = results.KEY__EXTRACOLUMNS__RESULT_TYPE
47 COLUMN__SOURCE_SKP = 'sourceSkpFile'
48 COLUMN__TILED_OR_WHOLE = 'tiledOrWhole'
49 COLUMN__TILENUM = 'tilenum'
50 COLUMN__BUILDER_A = 'builderA'
51 COLUMN__RENDER_MODE_A = 'renderModeA'
52 COLUMN__BUILDER_B = 'builderB'
53 COLUMN__RENDER_MODE_B = 'renderModeB'
54 # Known values for some of those columns.
55 COLUMN__TILED_OR_WHOLE__TILED = 'tiled'
56 COLUMN__TILED_OR_WHOLE__WHOLE = 'whole'
57
58 FREEFORM_COLUMN_IDS = [
59 COLUMN__SOURCE_SKP,
60 COLUMN__TILENUM,
61 ]
62 ORDERED_COLUMN_IDS = [
63 COLUMN__RESULT_TYPE,
64 COLUMN__SOURCE_SKP,
65 COLUMN__TILED_OR_WHOLE,
66 COLUMN__TILENUM,
67 COLUMN__BUILDER_A,
68 COLUMN__RENDER_MODE_A,
69 COLUMN__BUILDER_B,
70 COLUMN__RENDER_MODE_B,
71 ]
72
73 # A special "repo:" URL type that we use to refer to Skia repo contents.
74 # (Useful for comparing against expectations files we store in our repo.)
75 REPO_URL_PREFIX = 'repo:'
76 REPO_BASEPATH = os.path.abspath(os.path.join(
77 os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir))
78
79 # Which sections within a JSON summary file can contain results.
80 ALLOWED_SECTION_NAMES = [
81 gm_json.JSONKEY_ACTUALRESULTS,
82 gm_json.JSONKEY_EXPECTEDRESULTS,
83 ]
84
85
86 class RenderedPicturesComparisons(results.BaseComparisons):
87 """Loads results from multiple render_pictures runs into an ImagePairSet.
88 """
89
90 def __init__(self,
91 setA_dir, setB_dir,
92 setA_section, setB_section,
93 image_diff_db,
94 image_base_gs_url=DEFAULT_IMAGE_BASE_GS_URL, diff_base_url=None,
95 setA_label=None, setB_label=None,
96 gs=None, truncate_results=False, prefetch_only=False,
97 download_all_images=False):
98 """Constructor: downloads images and generates diffs.
99
100 Once the object has been created (which may take a while), you can call its
101 get_packaged_results_of_type() method to quickly retrieve the results...
102 unless you have set prefetch_only to True, in which case we will
103 asynchronously warm up the ImageDiffDB cache but not fill in self._results.
104
105 Args:
106 setA_dir: root directory to copy all JSON summaries from, and to use as
107 setA within the comparisons. This directory may be specified as a
108 gs:// URL, special "repo:" URL, or local filepath.
109 setB_dir: root directory to copy all JSON summaries from, and to use as
110 setB within the comparisons. This directory may be specified as a
111 gs:// URL, special "repo:" URL, or local filepath.
112 setA_section: which section within setA to examine; must be one of
113 ALLOWED_SECTION_NAMES
114 setB_section: which section within setB to examine; must be one of
115 ALLOWED_SECTION_NAMES
116 image_diff_db: ImageDiffDB instance
117 image_base_gs_url: "gs://" URL pointing at the Google Storage bucket/dir
118 under which all render_pictures result images can
119 be found; this will be used to read images for comparison within
120 this code, and included in the ImagePairSet (as an HTTP URL) so its
121 consumers know where to download the images from
122 diff_base_url: base URL within which the client should look for diff
123 images; if not specified, defaults to a "file:///" URL representation
124 of image_diff_db's storage_root
125 setA_label: description to use for results in setA; if None, will be
126 set to a reasonable default
127 setB_label: description to use for results in setB; if None, will be
128 set to a reasonable default
129 gs: instance of GSUtils object we can use to download summary files
130 truncate_results: FOR MANUAL TESTING: if True, truncate the set of images
131 we process, to speed up testing.
132 prefetch_only: if True, return the new object as quickly as possible
133 with empty self._results (just queue up all the files to process,
134 don't wait around for them to be processed and recorded); otherwise,
135 block until the results have been assembled and recorded in
136 self._results.
137 download_all_images: if True, download all images, even if we don't
138 need them to generate diffs. This will take much longer to complete,
139 but is useful for warming up the bitmap cache on local disk.
140 """
141 super(RenderedPicturesComparisons, self).__init__()
142 self._image_diff_db = image_diff_db
143 self._image_base_gs_url = image_base_gs_url
144 self._diff_base_url = (
145 diff_base_url or
146 url_utils.create_filepath_url(image_diff_db.storage_root))
147 self._gs = gs
148 self.truncate_results = truncate_results
149 self._prefetch_only = prefetch_only
150 self._download_all_images = download_all_images
151
152 # If we are comparing two different section types, we can use those
153 # as the default labels for setA and setB.
154 if setA_section != setB_section:
155 self._setA_label = setA_label or setA_section
156 self._setB_label = setB_label or setB_section
157 else:
158 self._setA_label = setA_label or 'setA'
159 self._setB_label = setB_label or 'setB'
160
161 tempdir = tempfile.mkdtemp()
162 try:
163 setA_root = os.path.join(tempdir, 'setA')
164 setB_root = os.path.join(tempdir, 'setB')
165 # TODO(stephana): There is a potential race condition here... we copy
166 # the contents out of the source_dir, and THEN we get the commithash
167 # of source_dir. If source_dir points at a git checkout, and that
168 # checkout is updated (by a different thread/process) during this
169 # operation, then the contents and commithash will be out of sync.
170 self._copy_dir_contents(source_dir=setA_dir, dest_dir=setA_root)
171 setA_repo_revision = self._get_repo_revision(source_dir=setA_dir)
172 self._copy_dir_contents(source_dir=setB_dir, dest_dir=setB_root)
173 setB_repo_revision = self._get_repo_revision(source_dir=setB_dir)
174
175 self._setA_descriptions = {
176 results.KEY__SET_DESCRIPTIONS__DIR: setA_dir,
177 results.KEY__SET_DESCRIPTIONS__REPO_REVISION: setA_repo_revision,
178 results.KEY__SET_DESCRIPTIONS__SECTION: setA_section,
179 }
180 self._setB_descriptions = {
181 results.KEY__SET_DESCRIPTIONS__DIR: setB_dir,
182 results.KEY__SET_DESCRIPTIONS__REPO_REVISION: setB_repo_revision,
183 results.KEY__SET_DESCRIPTIONS__SECTION: setB_section,
184 }
185
186 time_start = int(time.time())
187 self._results = self._load_result_pairs(
188 setA_root=setA_root, setB_root=setB_root,
189 setA_section=setA_section, setB_section=setB_section)
190 if self._results:
191 self._timestamp = int(time.time())
192 logging.info('Number of download file collisions: %s' %
193 imagediffdb.global_file_collisions)
194 logging.info('Results complete; took %d seconds.' %
195 (self._timestamp - time_start))
196 finally:
197 shutil.rmtree(tempdir)
198
199 def _load_result_pairs(self, setA_root, setB_root,
200 setA_section, setB_section):
201 """Loads all JSON image summaries from 2 directory trees and compares them.
202
203 TODO(stephana): This method is only called from within __init__(); it might
204 make more sense to just roll the content of this method into __init__().
205
206 Args:
207 setA_root: root directory containing JSON summaries of rendering results
208 setB_root: root directory containing JSON summaries of rendering results
209 setA_section: which section (gm_json.JSONKEY_ACTUALRESULTS or
210 gm_json.JSONKEY_EXPECTEDRESULTS) to load from the summaries in setA
211 setB_section: which section (gm_json.JSONKEY_ACTUALRESULTS or
212 gm_json.JSONKEY_EXPECTEDRESULTS) to load from the summaries in setB
213
214 Returns the summary of all image diff results (or None, depending on
215 self._prefetch_only).
216 """
217 logging.info('Reading JSON image summaries from dirs %s and %s...' % (
218 setA_root, setB_root))
219 setA_dicts = self.read_dicts_from_root(setA_root)
220 setB_dicts = self.read_dicts_from_root(setB_root)
221 logging.info('Comparing summary dicts...')
222
223 all_image_pairs = imagepairset.ImagePairSet(
224 descriptions=(self._setA_label, self._setB_label),
225 diff_base_url=self._diff_base_url)
226 failing_image_pairs = imagepairset.ImagePairSet(
227 descriptions=(self._setA_label, self._setB_label),
228 diff_base_url=self._diff_base_url)
229
230 # Override settings for columns that should be filtered using freeform text.
231 for column_id in FREEFORM_COLUMN_IDS:
232 factory = column.ColumnHeaderFactory(
233 header_text=column_id, use_freeform_filter=True)
234 all_image_pairs.set_column_header_factory(
235 column_id=column_id, column_header_factory=factory)
236 failing_image_pairs.set_column_header_factory(
237 column_id=column_id, column_header_factory=factory)
238
239 all_image_pairs.ensure_extra_column_values_in_summary(
240 column_id=COLUMN__RESULT_TYPE, values=[
241 results.KEY__RESULT_TYPE__FAILED,
242 results.KEY__RESULT_TYPE__NOCOMPARISON,
243 results.KEY__RESULT_TYPE__SUCCEEDED,
244 ])
245 failing_image_pairs.ensure_extra_column_values_in_summary(
246 column_id=COLUMN__RESULT_TYPE, values=[
247 results.KEY__RESULT_TYPE__FAILED,
248 results.KEY__RESULT_TYPE__NOCOMPARISON,
249 ])
250
251 logging.info('Starting to add imagepairs to queue.')
252 self._image_diff_db.log_queue_size_if_changed(limit_verbosity=False)
253
254 union_dict_paths = sorted(set(setA_dicts.keys() + setB_dicts.keys()))
255 num_union_dict_paths = len(union_dict_paths)
256 dict_num = 0
257 for dict_path in union_dict_paths:
258 dict_num += 1
259 logging.info(
260 'Asynchronously requesting pixel diffs for dict #%d of %d, "%s"...' %
261 (dict_num, num_union_dict_paths, dict_path))
262
263 dictA = self.get_default(setA_dicts, None, dict_path)
264 self._validate_dict_version(dictA)
265 dictA_results = self.get_default(dictA, {}, setA_section)
266
267 dictB = self.get_default(setB_dicts, None, dict_path)
268 self._validate_dict_version(dictB)
269 dictB_results = self.get_default(dictB, {}, setB_section)
270
271 image_A_base_url = self.get_default(
272 setA_dicts, self._image_base_gs_url, dict_path,
273 gm_json.JSONKEY_IMAGE_BASE_GS_URL)
274 image_B_base_url = self.get_default(
275 setB_dicts, self._image_base_gs_url, dict_path,
276 gm_json.JSONKEY_IMAGE_BASE_GS_URL)
277
278 # get the builders and render modes for each set
279 builder_A = self.get_default(dictA, None,
280 gm_json.JSONKEY_DESCRIPTIONS,
281 gm_json.JSONKEY_DESCRIPTIONS_BUILDER)
282 render_mode_A = self.get_default(dictA, None,
283 gm_json.JSONKEY_DESCRIPTIONS,
284 gm_json.JSONKEY_DESCRIPTIONS_RENDER_MODE)
285 builder_B = self.get_default(dictB, None,
286 gm_json.JSONKEY_DESCRIPTIONS,
287 gm_json.JSONKEY_DESCRIPTIONS_BUILDER)
288 render_mode_B = self.get_default(dictB, None,
289 gm_json.JSONKEY_DESCRIPTIONS,
290 gm_json.JSONKEY_DESCRIPTIONS_RENDER_MODE)
291
292 skp_names = sorted(set(dictA_results.keys() + dictB_results.keys()))
293 # Just for manual testing... truncate to an arbitrary subset.
294 if self.truncate_results:
295 skp_names = skp_names[1:3]
296 for skp_name in skp_names:
297 imagepairs_for_this_skp = []
298
299 whole_image_A = self.get_default(
300 dictA_results, None,
301 skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE)
302 whole_image_B = self.get_default(
303 dictB_results, None,
304 skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE)
305
306 imagepairs_for_this_skp.append(self._create_image_pair(
307 image_dict_A=whole_image_A, image_dict_B=whole_image_B,
308 image_A_base_url=image_A_base_url,
309 image_B_base_url=image_B_base_url,
310 builder_A=builder_A, render_mode_A=render_mode_A,
311 builder_B=builder_B, render_mode_B=render_mode_B,
312 source_json_file=dict_path,
313 source_skp_name=skp_name, tilenum=None))
314
315 tiled_images_A = self.get_default(
316 dictA_results, [],
317 skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES)
318 tiled_images_B = self.get_default(
319 dictB_results, [],
320 skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES)
321 if tiled_images_A or tiled_images_B:
322 num_tiles_A = len(tiled_images_A)
323 num_tiles_B = len(tiled_images_B)
324 num_tiles = max(num_tiles_A, num_tiles_B)
325 for tile_num in range(num_tiles):
326 imagepairs_for_this_skp.append(self._create_image_pair(
327 image_dict_A=(tiled_images_A[tile_num]
328 if tile_num < num_tiles_A else None),
329 image_dict_B=(tiled_images_B[tile_num]
330 if tile_num < num_tiles_B else None),
331 image_A_base_url=image_A_base_url,
332 image_B_base_url=image_B_base_url,
333 builder_A=builder_A, render_mode_A=render_mode_A,
334 builder_B=builder_B, render_mode_B=render_mode_B,
335 source_json_file=dict_path,
336 source_skp_name=skp_name, tilenum=tile_num))
337
338 for one_imagepair in imagepairs_for_this_skp:
339 if one_imagepair:
340 all_image_pairs.add_image_pair(one_imagepair)
341 result_type = one_imagepair.extra_columns_dict\
342 [COLUMN__RESULT_TYPE]
343 if result_type != results.KEY__RESULT_TYPE__SUCCEEDED:
344 failing_image_pairs.add_image_pair(one_imagepair)
345
346 logging.info('Finished adding imagepairs to queue.')
347 self._image_diff_db.log_queue_size_if_changed(limit_verbosity=False)
348
349 if self._prefetch_only:
350 return None
351 else:
352 return {
353 results.KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(
354 column_ids_in_order=ORDERED_COLUMN_IDS),
355 results.KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(
356 column_ids_in_order=ORDERED_COLUMN_IDS),
357 }
358
359 def _validate_dict_version(self, result_dict):
360 """Raises Exception if the dict is not the type/version we know how to read.
361
362 Args:
363 result_dict: dictionary holding output of render_pictures; if None,
364 this method will return without raising an Exception
365 """
366 # TODO(stephana): These values should be defined as constants somewhere,
367 # to be kept in sync between this file and writable_expectations.py
368 expected_header_type = 'ChecksummedImages'
369 expected_header_revision = 1
370
371 if result_dict == None:
372 return
373 header = result_dict[gm_json.JSONKEY_HEADER]
374 header_type = header[gm_json.JSONKEY_HEADER_TYPE]
375 if header_type != expected_header_type:
376 raise Exception('expected header_type "%s", but got "%s"' % (
377 expected_header_type, header_type))
378 header_revision = header[gm_json.JSONKEY_HEADER_REVISION]
379 if header_revision != expected_header_revision:
380 raise Exception('expected header_revision %d, but got %d' % (
381 expected_header_revision, header_revision))
382
383 def _create_image_pair(self, image_dict_A, image_dict_B,
384 image_A_base_url, image_B_base_url,
385 builder_A, render_mode_A,
386 builder_B, render_mode_B,
387 source_json_file,
388 source_skp_name, tilenum):
389 """Creates an ImagePair object for this pair of images.
390
391 Args:
392 image_dict_A: dict with JSONKEY_IMAGE_* keys, or None if no image
393 image_dict_B: dict with JSONKEY_IMAGE_* keys, or None if no image
394 image_A_base_url: base URL for image A
395 image_B_base_url: base URL for image B
396 builder_A: builder that created image set A or None if unknow
397 render_mode_A: render mode used to generate image set A or None if
398 unknown.
399 builder_B: builder that created image set A or None if unknow
400 render_mode_B: render mode used to generate image set A or None if
401 unknown.
402 source_json_file: string; relative path of the JSON file where this
403 result came from, within setA and setB.
404 source_skp_name: string; name of the source SKP file
405 tilenum: which tile, or None if a wholeimage
406
407 Returns:
408 An ImagePair object, or None if both image_dict_A and image_dict_B are
409 None.
410 """
411 if (not image_dict_A) and (not image_dict_B):
412 return None
413
414 def _checksum_and_relative_url(dic):
415 if dic:
416 return ((dic[gm_json.JSONKEY_IMAGE_CHECKSUMALGORITHM],
417 int(dic[gm_json.JSONKEY_IMAGE_CHECKSUMVALUE])),
418 dic[gm_json.JSONKEY_IMAGE_FILEPATH])
419 else:
420 return None, None
421
422 imageA_checksum, imageA_relative_url = _checksum_and_relative_url(
423 image_dict_A)
424 imageB_checksum, imageB_relative_url = _checksum_and_relative_url(
425 image_dict_B)
426
427 if not imageA_checksum:
428 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON
429 elif not imageB_checksum:
430 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON
431 elif imageA_checksum == imageB_checksum:
432 result_type = results.KEY__RESULT_TYPE__SUCCEEDED
433 else:
434 result_type = results.KEY__RESULT_TYPE__FAILED
435
436 extra_columns_dict = {
437 COLUMN__RESULT_TYPE: result_type,
438 COLUMN__SOURCE_SKP: source_skp_name,
439 COLUMN__BUILDER_A: builder_A,
440 COLUMN__RENDER_MODE_A: render_mode_A,
441 COLUMN__BUILDER_B: builder_B,
442 COLUMN__RENDER_MODE_B: render_mode_B,
443 }
444 if tilenum == None:
445 extra_columns_dict[COLUMN__TILED_OR_WHOLE] = COLUMN__TILED_OR_WHOLE__WHOLE
446 extra_columns_dict[COLUMN__TILENUM] = 'N/A'
447 else:
448 extra_columns_dict[COLUMN__TILED_OR_WHOLE] = COLUMN__TILED_OR_WHOLE__TILED
449 extra_columns_dict[COLUMN__TILENUM] = str(tilenum)
450
451 try:
452 return imagepair.ImagePair(
453 image_diff_db=self._image_diff_db,
454 imageA_base_url=image_A_base_url,
455 imageB_base_url=image_B_base_url,
456 imageA_relative_url=imageA_relative_url,
457 imageB_relative_url=imageB_relative_url,
458 extra_columns=extra_columns_dict,
459 source_json_file=source_json_file,
460 download_all_images=self._download_all_images)
461 except (KeyError, TypeError):
462 logging.exception(
463 'got exception while creating ImagePair for'
464 ' urlPair=("%s","%s"), source_skp_name="%s", tilenum="%s"' % (
465 imageA_relative_url, imageB_relative_url, source_skp_name,
466 tilenum))
467 return None
468
469 def _copy_dir_contents(self, source_dir, dest_dir):
470 """Copy all contents of source_dir into dest_dir, recursing into subdirs.
471
472 Args:
473 source_dir: path to source dir (GS URL, local filepath, or a special
474 "repo:" URL type that points at a file within our Skia checkout)
475 dest_dir: path to destination dir (local filepath)
476
477 The copy operates as a "merge with overwrite": any files in source_dir will
478 be "overlaid" on top of the existing content in dest_dir. Existing files
479 with the same names will be overwritten.
480 """
481 if gs_utils.GSUtils.is_gs_url(source_dir):
482 (bucket, path) = gs_utils.GSUtils.split_gs_url(source_dir)
483 self._gs.download_dir_contents(source_bucket=bucket, source_dir=path,
484 dest_dir=dest_dir)
485 elif source_dir.lower().startswith(REPO_URL_PREFIX):
486 repo_dir = os.path.join(REPO_BASEPATH, source_dir[len(REPO_URL_PREFIX):])
487 shutil.copytree(repo_dir, dest_dir)
488 else:
489 shutil.copytree(source_dir, dest_dir)
490
491 def _get_repo_revision(self, source_dir):
492 """Get the commit hash of source_dir, IF it refers to a git checkout.
493
494 Args:
495 source_dir: path to source dir (GS URL, local filepath, or a special
496 "repo:" URL type that points at a file within our Skia checkout;
497 only the "repo:" URL type will have a commit hash.
498 """
499 if source_dir.lower().startswith(REPO_URL_PREFIX):
500 repo_dir = os.path.join(REPO_BASEPATH, source_dir[len(REPO_URL_PREFIX):])
501 return subprocess.check_output(
502 args=[git_utils.GIT, 'rev-parse', 'HEAD'], cwd=repo_dir).strip()
503 else:
504 return None
OLDNEW
« no previous file with comments | « gm/rebaseline_server/compare_configs_test.py ('k') | gm/rebaseline_server/compare_rendered_pictures_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698