1#!/usr/bin/env python 2# Copyright 2016 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import math 7 8import json5_generator 9import template_expander 10import keyword_utils 11import bisect 12 13from blinkbuild.name_style_converter import NameStyleConverter 14from core.css import css_properties 15from core.style.computed_style_fields import DiffGroup, Enum, Group, Field 16 17from itertools import chain 18 19# Heuristic ordering of types from largest to smallest, used to sort fields by 20# their alignment sizes. 21# Specifying the exact alignment sizes for each type is impossible because it's 22# platform specific, so we define an ordering instead. 23# The ordering comes from the data obtained in: 24# https://codereview.chromium.org/2841413002 25# FIXME: Put alignment sizes into code form, rather than linking to a CL 26# which may disappear. 27ALIGNMENT_ORDER = [ 28 # Aligns like double 29 'ScaleTransformOperation', 30 'RotateTransformOperation', 31 'TranslateTransformOperation', 32 'base::Optional<IntSize>', 33 'double', 34 # Aligns like a pointer (can be 32 or 64 bits) 35 'NamedGridLinesMap', 36 'OrderedNamedGridLines', 37 'NamedGridAreaMap', 38 'TransformOperations', 39 'Vector<CSSPropertyID>', 40 'Vector<GridTrackSize>', 41 'Vector<AtomicString>', 42 'GridPosition', 43 'GapLength', 44 'AtomicString', 45 'scoped_refptr', 46 'Persistent', 47 'std::unique_ptr', 48 'Vector<String>', 49 'Font', 50 'FillLayer', 51 'NinePieceImage', 52 'IntrinsicLength', 53 # Aligns like float 54 'StyleOffsetRotation', 55 'TransformOrigin', 56 'ScrollPadding', 57 'ScrollMargin', 58 'LengthBox', 59 'LengthSize', 60 'FloatSize', 61 'LengthPoint', 62 'Length', 63 'TextSizeAdjust', 64 'TabSize', 65 'float', 66 # Aligns like int 67 'cc::ScrollSnapType', 68 'cc::ScrollSnapAlign', 69 'BorderValue', 70 'StyleColor', 71 'Color', 72 'LayoutUnit', 73 'LineClampValue', 74 'OutlineValue', 75 'unsigned', 76 'size_t', 77 'int', 78 # Aligns like short 79 'unsigned short', 80 'short', 81 # Aligns like char 82 'StyleSelfAlignmentData', 83 'StyleContentAlignmentData', 84 'uint8_t', 85 'char', 86 # Aligns like bool 87 'bool' 88] 89 90# FIXME: Improve documentation and add docstrings. 91 92 93def _flatten_list(x): 94 """Flattens a list of lists into a single list.""" 95 return list(chain.from_iterable(x)) 96 97 98def _get_include_paths(properties): 99 """ 100 Get a list of paths that need to be included for ComputedStyleBase. 101 """ 102 include_paths = set() 103 for property_ in properties: 104 include_paths.update(property_['include_paths']) 105 return list(sorted(include_paths)) 106 107 108def _create_groups(properties): 109 """Create a tree of groups from a list of properties. 110 111 Returns: 112 Group: The root group of the tree. The name of the group is set to None. 113 """ 114 115 # We first convert properties into a dictionary structure. Each dictionary 116 # represents a group. The None key corresponds to the fields directly stored 117 # on that group. The other keys map from group name to another dictionary. 118 # For example: 119 # { 120 # None: [field1, field2, ...] 121 # 'groupA': { None: [field3] }, 122 # 'groupB': { 123 # None: [], 124 # 'groupC': { None: [field4] }, 125 # }, 126 # } 127 # 128 # We then recursively convert this dictionary into a tree of Groups. 129 # FIXME: Skip the first step by changing Group attributes to methods. 130 def _dict_to_group(name, group_dict): 131 fields_in_current_group = group_dict.pop(None) 132 subgroups = [ 133 _dict_to_group(subgroup_name, subgroup_dict) 134 for subgroup_name, subgroup_dict in group_dict.items() 135 ] 136 return Group(name, subgroups, _reorder_fields(fields_in_current_group)) 137 138 root_group_dict = {None: []} 139 for property_ in properties: 140 current_group_dict = root_group_dict 141 if property_['field_group']: 142 for group_name in property_['field_group'].split('->'): 143 current_group_dict[group_name] = current_group_dict.get( 144 group_name, {None: []}) 145 current_group_dict = current_group_dict[group_name] 146 current_group_dict[None].extend(_create_fields(property_)) 147 148 return _dict_to_group(None, root_group_dict) 149 150 151def _create_diff_groups_map(diff_function_inputs, root_group): 152 diff_functions_map = {} 153 154 for entry in diff_function_inputs: 155 # error handling 156 field_names = entry['fields_to_diff'] + _list_field_dependencies( 157 entry['methods_to_diff'] + entry['predicates_to_test']) 158 for name in field_names: 159 assert name in [ 160 field.property_name for field in root_group.all_fields], \ 161 "The field '{}' isn't a defined field on ComputedStyle. " \ 162 "Please check that there's an entry for '{}' in" \ 163 "css_properties.json5 or " \ 164 "computed_style_extra_fields.json5".format(name, name) 165 diff_functions_map[entry['name'].original] = _create_diff_groups( 166 entry['fields_to_diff'], entry['methods_to_diff'], 167 entry['predicates_to_test'], root_group) 168 return diff_functions_map 169 170 171def _list_field_dependencies(entries_with_field_dependencies): 172 field_dependencies = [] 173 for entry in entries_with_field_dependencies: 174 field_dependencies += entry['field_dependencies'] 175 return field_dependencies 176 177 178def _create_diff_groups(fields_to_diff, methods_to_diff, predicates_to_test, 179 root_group): 180 diff_group = DiffGroup(root_group) 181 field_dependencies = _list_field_dependencies(methods_to_diff + 182 predicates_to_test) 183 for subgroup in root_group.subgroups: 184 if any( 185 field.property_name in (fields_to_diff + field_dependencies) 186 for field in subgroup.all_fields): 187 diff_group.subgroups.append( 188 _create_diff_groups(fields_to_diff, methods_to_diff, 189 predicates_to_test, subgroup)) 190 for entry in fields_to_diff: 191 for field in root_group.fields: 192 if not field.is_inherited_flag and entry == field.property_name: 193 diff_group.fields.append(field) 194 for entry in methods_to_diff: 195 for field in root_group.fields: 196 if (not field.is_inherited_flag 197 and field.property_name in entry['field_dependencies'] 198 and entry['method'] not in diff_group.expressions): 199 diff_group.expressions.append(entry['method']) 200 for entry in predicates_to_test: 201 for field in root_group.fields: 202 if (not field.is_inherited_flag 203 and field.property_name in entry['field_dependencies'] 204 and entry['predicate'] not in diff_group.predicates): 205 diff_group.predicates.append(entry['predicate']) 206 return diff_group 207 208 209def _create_enums(properties): 210 """Returns a list of Enums to be generated""" 211 enums = {} 212 for property_ in properties: 213 # Only generate enums for keyword properties that do not 214 # require includes. 215 if (property_['field_template'] in ('keyword', 'multi_keyword') 216 and len(property_['include_paths']) == 0): 217 enum = Enum( 218 property_['type_name'], 219 property_['keywords'], 220 is_set=(property_['field_template'] == 'multi_keyword')) 221 if property_['field_template'] == 'multi_keyword': 222 assert property_['keywords'][0] == 'none', \ 223 "First keyword in a 'multi_keyword' field must be " \ 224 "'none' in '{}'.".format(property_['name']) 225 226 if enum.type_name in enums: 227 # There's an enum with the same name, check if the enum 228 # values are the same 229 assert set(enums[enum.type_name].values) == set(enum.values), \ 230 "'{}' can't have type_name '{}' because it was used by " \ 231 "a previous property, but with a different set of " \ 232 "keywords. Either give it a different name or ensure " \ 233 "the keywords are the same.".format( 234 property_['name'], enum.type_name) 235 else: 236 enums[enum.type_name] = enum 237 238 # Return the enums sorted by type name 239 return list(sorted(enums.values(), key=lambda e: e.type_name)) 240 241 242def _create_property_field(property_): 243 """ 244 Create a property field. 245 """ 246 name_for_methods = property_['name_for_methods'] 247 248 assert property_['default_value'] is not None, \ 249 'MakeComputedStyleBase requires an default value for all fields, ' \ 250 'none specified for property ' + property_['name'] 251 252 type_name = property_['type_name'] 253 if property_['field_template'] == 'keyword': 254 assert property_['field_size'] is None, \ 255 ("'" + property_['name'] + "' is a keyword field, " 256 "so it should not specify a field_size") 257 size = int(math.ceil(math.log(len(property_['keywords']), 2))) 258 elif property_['field_template'] == 'multi_keyword': 259 size = len(property_['keywords']) - 1 # Subtract 1 for 'none' keyword 260 elif property_['field_template'] == 'external': 261 size = None 262 elif property_['field_template'] == 'primitive': 263 # pack bools with 1 bit. 264 size = 1 if type_name == 'bool' else property_["field_size"] 265 elif property_['field_template'] == 'pointer': 266 size = None 267 else: 268 assert property_['field_template'] == 'monotonic_flag', \ 269 "Please use a valid value for field_template" 270 size = 1 271 272 return Field( 273 'property', 274 name_for_methods, 275 property_name=property_['name'].original, 276 inherited=property_['inherited'], 277 independent=property_['independent'], 278 type_name=property_['type_name'], 279 wrapper_pointer_name=property_['wrapper_pointer_name'], 280 field_template=property_['field_template'], 281 size=size, 282 default_value=property_['default_value'], 283 custom_copy=property_['custom_copy'], 284 custom_compare=property_['custom_compare'], 285 mutable=property_['mutable'], 286 getter_method_name=property_['getter'], 287 setter_method_name=property_['setter'], 288 initial_method_name=property_['initial'], 289 computed_style_custom_functions=property_[ 290 'computed_style_custom_functions'], 291 ) 292 293 294def _create_inherited_flag_field(property_): 295 """ 296 Create the field used for an inheritance fast path from an independent CSS 297 property, and return the Field object. 298 """ 299 name_for_methods = NameStyleConverter( 300 property_['name_for_methods']).to_function_name( 301 suffix=['is', 'inherited']) 302 name_source = NameStyleConverter(name_for_methods) 303 return Field( 304 'inherited_flag', 305 name_for_methods, 306 property_name=property_['name'].original, 307 type_name='bool', 308 wrapper_pointer_name=None, 309 field_template='primitive', 310 size=1, 311 default_value='true', 312 custom_copy=False, 313 custom_compare=False, 314 mutable=False, 315 getter_method_name=name_source.to_function_name(), 316 setter_method_name=name_source.to_function_name(prefix='set'), 317 initial_method_name=name_source.to_function_name(prefix='initial'), 318 computed_style_custom_functions=property_[ 319 "computed_style_custom_functions"], 320 ) 321 322 323def _create_fields(property_): 324 """ 325 Create ComputedStyle fields from a property and return a list of Fields. 326 """ 327 fields = [] 328 # Only generate properties that have a field template 329 if property_['field_template'] is not None: 330 # If the property is independent, add the single-bit sized isInherited 331 # flag to the list of Fields as well. 332 if property_['independent']: 333 fields.append(_create_inherited_flag_field(property_)) 334 335 fields.append(_create_property_field(property_)) 336 337 return fields 338 339 340def _reorder_bit_fields(bit_fields): 341 # Since fields cannot cross word boundaries, in order to minimize 342 # padding, group fields into buckets so that as many buckets as possible 343 # are exactly 32 bits. Although this greedy approach may not always 344 # produce the optimal solution, we add a static_assert to the code to 345 # ensure ComputedStyleBase results in the expected size. If that 346 # static_assert fails, this code is falling into the small number of 347 # cases that are suboptimal, and may need to be rethought. 348 # For more details on packing bit fields to reduce padding, see: 349 # http://www.catb.org/esr/structure-packing/#_bitfields 350 field_buckets = [] 351 # Consider fields in descending order of size to reduce fragmentation 352 # when they are selected. Ties broken in alphabetical order by name. 353 for field in sorted(bit_fields, key=lambda f: (-f.size, f.name)): 354 added_to_bucket = False 355 # Go through each bucket and add this field if it will not increase 356 # the bucket's size to larger than 32 bits. Otherwise, make a new 357 # bucket containing only this field. 358 for bucket in field_buckets: 359 if sum(f.size for f in bucket) + field.size <= 32: 360 bucket.append(field) 361 added_to_bucket = True 362 break 363 if not added_to_bucket: 364 field_buckets.append([field]) 365 366 return _flatten_list(field_buckets) 367 368 369def _reorder_non_bit_fields(non_bit_fields): 370 # A general rule of thumb is to sort members by their alignment requirement 371 # (from biggest aligned to smallest). 372 for field in non_bit_fields: 373 assert field.alignment_type in ALIGNMENT_ORDER, \ 374 "Type {} has unknown alignment. Please update ALIGNMENT_ORDER " \ 375 "to include it.".format(field.name) 376 return list( 377 sorted( 378 non_bit_fields, 379 key=lambda f: ALIGNMENT_ORDER.index(f.alignment_type))) 380 381 382def _reorder_fields(fields): 383 """ 384 Returns a list of fields ordered to minimise padding. 385 """ 386 # Separate out bit fields from non bit fields 387 bit_fields = [field for field in fields if field.is_bit_field] 388 non_bit_fields = [field for field in fields if not field.is_bit_field] 389 390 # Non bit fields go first, then the bit fields. 391 return _reorder_non_bit_fields(non_bit_fields) + _reorder_bit_fields( 392 bit_fields) 393 394 395def _get_properties_ranking_using_partition_rule(properties_ranking, 396 partition_rule): 397 """Take the contents of the properties ranking file and produce a dictionary 398 of css properties with their group number based on the partition_rule 399 400 Args: 401 properties_ranking: rankings map as read from CSSPropertyRanking.json5 402 partition_rule: cumulative distribution over properties_ranking 403 404 Returns: 405 dictionary with keys are css properties' name values are the group 406 that each css properties belong to. Smaller group number is higher 407 popularity in the ranking. 408 """ 409 return dict( 410 zip(properties_ranking, [ 411 bisect.bisect_left(partition_rule, 412 float(i) / len(properties_ranking)) + 1 413 for i in range(len(properties_ranking)) 414 ])) 415 416 417def _best_rank(prop, ranking_map): 418 """Return the best ranking value for the specified property. 419 420 This function collects ranking values for not only the property's real name 421 but also its aliases, and returns the best (lower is better) value. 422 If no ranking values for the property is available, this returns -1. 423 """ 424 worst_rank = max(ranking_map.values()) + 1 425 best_rank = ranking_map.get(prop["name"].original, worst_rank) 426 427 for alias_name in prop.get("aliases", []): 428 best_rank = min(best_rank, ranking_map.get(alias_name, worst_rank)) 429 return best_rank if best_rank != worst_rank else -1 430 431 432def _evaluate_rare_non_inherited_group(properties, 433 properties_ranking, 434 num_layers, 435 partition_rule=None): 436 """Re-evaluate the grouping of RareNonInherited groups based on each 437 property's popularity. 438 439 Args: 440 properties: list of all css properties 441 properties_ranking: map of property rankings 442 num_layers: the number of group to split 443 partition_rule: cumulative distribution over properties_ranking 444 Ex: [0.3, 0.6, 1] 445 """ 446 if partition_rule is None: 447 partition_rule = [ 448 1.0 * (i + 1) / num_layers for i in range(num_layers) 449 ] 450 451 assert num_layers == len(partition_rule), \ 452 "Length of rule and num_layers mismatch" 453 454 layers_name = [ 455 "rare-non-inherited-usage-less-than-{}-percent".format( 456 int(round(partition_rule[i] * 100))) for i in range(num_layers) 457 ] 458 properties_ranking = _get_properties_ranking_using_partition_rule( 459 properties_ranking, partition_rule) 460 461 for property_ in properties: 462 rank = _best_rank(property_, properties_ranking) 463 if (property_["field_group"] is not None 464 and "*" in property_["field_group"] 465 and not property_["inherited"] and rank >= 0): 466 467 assert property_["field_group"] == "*", \ 468 "The property {} will be automatically assigned a group, " \ 469 "please put '*' as the field_group".format(property_['name']) 470 471 property_["field_group"] = "->".join(layers_name[0:rank]) 472 elif (property_["field_group"] is not None 473 and "*" in property_["field_group"] 474 and not property_["inherited"] and rank < 0): 475 group_tree = property_["field_group"].split("->")[1:] 476 group_tree = [layers_name[0], layers_name[0] + "-sub"] + group_tree 477 property_["field_group"] = "->".join(group_tree) 478 479 480def _evaluate_rare_inherit_group(properties, 481 properties_ranking, 482 num_layers, 483 partition_rule=None): 484 """Re-evaluate the grouping of RareInherited groups based on each property's 485 popularity. 486 487 Args: 488 properties: list of all css properties 489 properties_ranking: map of property rankings 490 num_layers: the number of group to split 491 partition_rule: cumulative distribution over properties_ranking 492 Ex: [0.4, 1] 493 """ 494 if partition_rule is None: 495 partition_rule = [ 496 1.0 * (i + 1) / num_layers for i in range(num_layers) 497 ] 498 499 assert num_layers == len(partition_rule), \ 500 "Length of rule and num_layers mismatch" 501 502 layers_name = [ 503 "rare-inherited-usage-less-than-{}-percent".format( 504 int(round(partition_rule[i] * 100))) for i in range(num_layers) 505 ] 506 507 properties_ranking = _get_properties_ranking_using_partition_rule( 508 properties_ranking, partition_rule) 509 510 for property_ in properties: 511 rank = _best_rank(property_, properties_ranking) 512 if (property_["field_group"] is not None 513 and "*" in property_["field_group"] and property_["inherited"] 514 and rank >= 0): 515 property_["field_group"] = "->".join(layers_name[0:rank]) 516 elif (property_["field_group"] is not None 517 and "*" in property_["field_group"] and property_["inherited"] 518 and rank < 0): 519 group_tree = property_["field_group"].split("->")[1:] 520 group_tree = [layers_name[0], layers_name[0] + "-sub"] + group_tree 521 property_["field_group"] = "->".join(group_tree) 522 523 524class ComputedStyleBaseWriter(json5_generator.Writer): 525 def __init__(self, json5_file_paths, output_dir): 526 super(ComputedStyleBaseWriter, self).__init__([], output_dir) 527 528 self._input_files = json5_file_paths 529 530 # Reads css_properties.json5, computed_style_field_aliases.json5, 531 # runtime_enabled_features.json5 and computed_style_extra_fields.json5 532 self._css_properties = css_properties.CSSProperties( 533 json5_file_paths[0:4]) 534 535 # We sort the enum values based on each value's position in 536 # the keywords as listed in css_properties.json5. This will ensure that 537 # if there is a continuous 538 # segment in css_properties.json5 matching the segment in this enum then 539 # the generated enum will have the same order and continuity as 540 # css_properties.json5 and we can get the longest continuous segment. 541 # Thereby reduce the switch case statement to the minimum. 542 properties = keyword_utils.sort_keyword_properties_by_canonical_order( 543 self._css_properties.longhands, json5_file_paths[5], 544 self.default_parameters) 545 self._properties = properties + self._css_properties.extra_fields 546 547 self._generated_enums = _create_enums(self._properties) 548 549 # Organise fields into a tree structure where the root group 550 # is ComputedStyleBase. 551 group_parameters = dict( 552 [(conf["name"], conf["cumulative_distribution"]) 553 for conf in json5_generator.Json5File.load_from_files( 554 [json5_file_paths[7]]).name_dictionaries]) 555 556 properties_ranking = [ 557 x["name"].original for x in json5_generator.Json5File. 558 load_from_files([json5_file_paths[6]]).name_dictionaries 559 ] 560 _evaluate_rare_non_inherited_group( 561 self._properties, properties_ranking, 562 len(group_parameters["rare_non_inherited_properties_rule"]), 563 group_parameters["rare_non_inherited_properties_rule"]) 564 _evaluate_rare_inherit_group( 565 self._properties, properties_ranking, 566 len(group_parameters["rare_inherited_properties_rule"]), 567 group_parameters["rare_inherited_properties_rule"]) 568 self._root_group = _create_groups(self._properties) 569 self._diff_functions_map = _create_diff_groups_map( 570 json5_generator.Json5File.load_from_files( 571 [json5_file_paths[4]]).name_dictionaries, self._root_group) 572 573 self._include_paths = _get_include_paths(self._properties) 574 self._outputs = { 575 'computed_style_base.h': 576 self.generate_base_computed_style_h, 577 'computed_style_base.cc': 578 self.generate_base_computed_style_cpp, 579 'computed_style_base_constants.h': 580 self.generate_base_computed_style_constants, 581 } 582 583 @template_expander.use_jinja( 584 'core/style/templates/computed_style_base.h.tmpl', 585 tests={ 586 'in': lambda a, b: a in b 587 }) 588 def generate_base_computed_style_h(self): 589 return { 590 'input_files': self._input_files, 591 'properties': self._properties, 592 'enums': self._generated_enums, 593 'include_paths': self._include_paths, 594 'computed_style': self._root_group, 595 'diff_functions_map': self._diff_functions_map, 596 } 597 598 @template_expander.use_jinja( 599 'core/style/templates/computed_style_base.cc.tmpl', 600 tests={ 601 'in': lambda a, b: a in b 602 }) 603 def generate_base_computed_style_cpp(self): 604 return { 605 'input_files': self._input_files, 606 'properties': self._properties, 607 'enums': self._generated_enums, 608 'include_paths': self._include_paths, 609 'computed_style': self._root_group, 610 'diff_functions_map': self._diff_functions_map, 611 } 612 613 @template_expander.use_jinja( 614 'core/style/templates/computed_style_base_constants.h.tmpl') 615 def generate_base_computed_style_constants(self): 616 return { 617 'input_files': self._input_files, 618 'properties': self._properties, 619 'enums': self._generated_enums, 620 } 621 622 623if __name__ == '__main__': 624 json5_generator.Maker(ComputedStyleBaseWriter).main() 625