PropertyMap.java revision 1416:a750a66640e0
1/* 2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package jdk.nashorn.internal.runtime; 27 28import static jdk.nashorn.internal.runtime.PropertyHashMap.EMPTY_HASHMAP; 29import static jdk.nashorn.internal.runtime.arrays.ArrayIndex.getArrayIndex; 30import static jdk.nashorn.internal.runtime.arrays.ArrayIndex.isValidArrayIndex; 31 32import java.io.IOException; 33import java.io.ObjectInputStream; 34import java.io.ObjectOutputStream; 35import java.io.Serializable; 36import java.lang.invoke.SwitchPoint; 37import java.lang.ref.SoftReference; 38import java.util.Arrays; 39import java.util.BitSet; 40import java.util.Collection; 41import java.util.HashMap; 42import java.util.Iterator; 43import java.util.NoSuchElementException; 44import java.util.WeakHashMap; 45import java.util.concurrent.atomic.LongAdder; 46import jdk.nashorn.internal.scripts.JO; 47 48/** 49 * Map of object properties. The PropertyMap is the "template" for JavaScript object 50 * layouts. It contains a map with prototype names as keys and {@link Property} instances 51 * as values. A PropertyMap is typically passed to the {@link ScriptObject} constructor 52 * to form the seed map for the ScriptObject. 53 * <p> 54 * All property maps are immutable. If a property is added, modified or removed, the mutator 55 * will return a new map. 56 */ 57public final class PropertyMap implements Iterable<Object>, Serializable { 58 /** Used for non extensible PropertyMaps, negative logic as the normal case is extensible. See {@link ScriptObject#preventExtensions()} */ 59 public static final int NOT_EXTENSIBLE = 0b0000_0001; 60 /** Does this map contain valid array keys? */ 61 public static final int CONTAINS_ARRAY_KEYS = 0b0000_0010; 62 63 /** Map status flags. */ 64 private int flags; 65 66 /** Map of properties. */ 67 private transient PropertyHashMap properties; 68 69 /** Number of fields in use. */ 70 private int fieldCount; 71 72 /** Number of fields available. */ 73 private final int fieldMaximum; 74 75 /** Length of spill in use. */ 76 private int spillLength; 77 78 /** Structure class name */ 79 private String className; 80 81 /** {@link SwitchPoint}s for gets on inherited properties. */ 82 private transient HashMap<String, SwitchPoint> protoGetSwitches; 83 84 /** History of maps, used to limit map duplication. */ 85 private transient WeakHashMap<Property, SoftReference<PropertyMap>> history; 86 87 /** History of prototypes, used to limit map duplication. */ 88 private transient WeakHashMap<ScriptObject, SoftReference<PropertyMap>> protoHistory; 89 90 /** property listeners */ 91 private transient PropertyListeners listeners; 92 93 private transient BitSet freeSlots; 94 95 private static final long serialVersionUID = -7041836752008732533L; 96 97 /** 98 * Constructor. 99 * 100 * @param properties A {@link PropertyHashMap} with initial contents. 101 * @param fieldCount Number of fields in use. 102 * @param fieldMaximum Number of fields available. 103 * @param spillLength Number of spill slots used. 104 * @param containsArrayKeys True if properties contain numeric keys 105 */ 106 private PropertyMap(final PropertyHashMap properties, final String className, final int fieldCount, 107 final int fieldMaximum, final int spillLength, final boolean containsArrayKeys) { 108 this.properties = properties; 109 this.className = className; 110 this.fieldCount = fieldCount; 111 this.fieldMaximum = fieldMaximum; 112 this.spillLength = spillLength; 113 if (containsArrayKeys) { 114 setContainsArrayKeys(); 115 } 116 117 if (Context.DEBUG) { 118 count.increment(); 119 } 120 } 121 122 /** 123 * Cloning constructor. 124 * 125 * @param propertyMap Existing property map. 126 * @param properties A {@link PropertyHashMap} with a new set of properties. 127 */ 128 private PropertyMap(final PropertyMap propertyMap, final PropertyHashMap properties) { 129 this.properties = properties; 130 this.flags = propertyMap.flags; 131 this.spillLength = propertyMap.spillLength; 132 this.fieldCount = propertyMap.fieldCount; 133 this.fieldMaximum = propertyMap.fieldMaximum; 134 // We inherit the parent property listeners instance. It will be cloned when a new listener is added. 135 this.listeners = propertyMap.listeners; 136 this.freeSlots = propertyMap.freeSlots; 137 138 if (Context.DEBUG) { 139 count.increment(); 140 clonedCount.increment(); 141 } 142 } 143 144 /** 145 * Cloning constructor. 146 * 147 * @param propertyMap Existing property map. 148 */ 149 private PropertyMap(final PropertyMap propertyMap) { 150 this(propertyMap, propertyMap.properties); 151 } 152 153 private void writeObject(final ObjectOutputStream out) throws IOException { 154 out.defaultWriteObject(); 155 out.writeObject(properties.getProperties()); 156 } 157 158 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 159 in.defaultReadObject(); 160 161 final Property[] props = (Property[]) in.readObject(); 162 this.properties = EMPTY_HASHMAP.immutableAdd(props); 163 164 assert className != null; 165 final Class<?> structure = Context.forStructureClass(className); 166 for (final Property prop : props) { 167 prop.initMethodHandles(structure); 168 } 169 } 170 171 /** 172 * Public property map allocator. 173 * 174 * <p>It is the caller's responsibility to make sure that {@code properties} does not contain 175 * properties with keys that are valid array indices.</p> 176 * 177 * @param properties Collection of initial properties. 178 * @param className class name 179 * @param fieldCount Number of fields in use. 180 * @param fieldMaximum Number of fields available. 181 * @param spillLength Number of used spill slots. 182 * @return New {@link PropertyMap}. 183 */ 184 public static PropertyMap newMap(final Collection<Property> properties, final String className, final int fieldCount, final int fieldMaximum, final int spillLength) { 185 final PropertyHashMap newProperties = EMPTY_HASHMAP.immutableAdd(properties); 186 return new PropertyMap(newProperties, className, fieldCount, fieldMaximum, spillLength, false); 187 } 188 189 /** 190 * Public property map allocator. Used by nasgen generated code. 191 * 192 * <p>It is the caller's responsibility to make sure that {@code properties} does not contain 193 * properties with keys that are valid array indices.</p> 194 * 195 * @param properties Collection of initial properties. 196 * @return New {@link PropertyMap}. 197 */ 198 public static PropertyMap newMap(final Collection<Property> properties) { 199 return properties == null || properties.isEmpty()? newMap() : newMap(properties, JO.class.getName(), 0, 0, 0); 200 } 201 202 /** 203 * Return a sharable empty map for the given object class. 204 * @param clazz the base object class 205 * @return New empty {@link PropertyMap}. 206 */ 207 public static PropertyMap newMap(final Class<? extends ScriptObject> clazz) { 208 return new PropertyMap(EMPTY_HASHMAP, clazz.getName(), 0, 0, 0, false); 209 } 210 211 /** 212 * Return a sharable empty map. 213 * 214 * @return New empty {@link PropertyMap}. 215 */ 216 public static PropertyMap newMap() { 217 return newMap(JO.class); 218 } 219 220 /** 221 * Return number of properties in the map. 222 * 223 * @return Number of properties. 224 */ 225 public int size() { 226 return properties.size(); 227 } 228 229 /** 230 * Get the listeners of this map, or null if none exists 231 * 232 * @return the listeners 233 */ 234 public PropertyListeners getListeners() { 235 return listeners; 236 } 237 238 /** 239 * Add {@code listenerMap} as a listener to this property map for the given {@code key}. 240 * 241 * @param key the property name 242 * @param listenerMap the listener map 243 */ 244 public void addListener(final String key, final PropertyMap listenerMap) { 245 if (listenerMap != this) { 246 // We need to clone listener instance when adding a new listener since we share 247 // the listeners instance with our parent maps that don't need to see the new listener. 248 listeners = PropertyListeners.addListener(listeners, key, listenerMap); 249 } 250 } 251 252 /** 253 * A new property is being added. 254 * 255 * @param property The new Property added. 256 */ 257 public void propertyAdded(final Property property) { 258 invalidateProtoGetSwitchPoint(property); 259 if (listeners != null) { 260 listeners.propertyAdded(property); 261 } 262 } 263 264 /** 265 * An existing property is being deleted. 266 * 267 * @param property The property being deleted. 268 */ 269 public void propertyDeleted(final Property property) { 270 invalidateProtoGetSwitchPoint(property); 271 if (listeners != null) { 272 listeners.propertyDeleted(property); 273 } 274 } 275 276 /** 277 * An existing property is being redefined. 278 * 279 * @param oldProperty The old property 280 * @param newProperty The new property 281 */ 282 public void propertyModified(final Property oldProperty, final Property newProperty) { 283 invalidateProtoGetSwitchPoint(oldProperty); 284 if (listeners != null) { 285 listeners.propertyModified(oldProperty, newProperty); 286 } 287 } 288 289 /** 290 * The prototype of an object associated with this {@link PropertyMap} is changed. 291 */ 292 public void protoChanged() { 293 invalidateAllProtoGetSwitchPoints(); 294 if (listeners != null) { 295 listeners.protoChanged(); 296 } 297 } 298 299 /** 300 * Return a SwitchPoint used to track changes of a property in a prototype. 301 * 302 * @param key Property key. 303 * @return A shared {@link SwitchPoint} for the property. 304 */ 305 public synchronized SwitchPoint getSwitchPoint(final String key) { 306 if (protoGetSwitches == null) { 307 protoGetSwitches = new HashMap<>(); 308 } 309 310 SwitchPoint switchPoint = protoGetSwitches.get(key); 311 if (switchPoint == null) { 312 switchPoint = new SwitchPoint(); 313 protoGetSwitches.put(key, switchPoint); 314 } 315 316 return switchPoint; 317 } 318 319 /** 320 * Indicate that a prototype property has changed. 321 * 322 * @param property {@link Property} to invalidate. 323 */ 324 synchronized void invalidateProtoGetSwitchPoint(final Property property) { 325 if (protoGetSwitches != null) { 326 327 final String key = property.getKey(); 328 final SwitchPoint sp = protoGetSwitches.get(key); 329 if (sp != null) { 330 protoGetSwitches.remove(key); 331 if (Context.DEBUG) { 332 protoInvalidations.increment(); 333 } 334 SwitchPoint.invalidateAll(new SwitchPoint[] { sp }); 335 } 336 } 337 } 338 339 /** 340 * Indicate that proto itself has changed in hierarchy somewhere. 341 */ 342 synchronized void invalidateAllProtoGetSwitchPoints() { 343 if (protoGetSwitches != null) { 344 final int size = protoGetSwitches.size(); 345 if (size > 0) { 346 if (Context.DEBUG) { 347 protoInvalidations.add(size); 348 } 349 SwitchPoint.invalidateAll(protoGetSwitches.values().toArray(new SwitchPoint[size])); 350 protoGetSwitches.clear(); 351 } 352 } 353 } 354 355 /** 356 * Add a property to the map, re-binding its getters and setters, 357 * if available, to a given receiver. This is typically the global scope. See 358 * {@link ScriptObject#addBoundProperties(ScriptObject)} 359 * 360 * @param property {@link Property} being added. 361 * @param bindTo Object to bind to. 362 * 363 * @return New {@link PropertyMap} with {@link Property} added. 364 */ 365 PropertyMap addPropertyBind(final AccessorProperty property, final Object bindTo) { 366 // No need to store bound property in the history as bound properties can't be reused. 367 return addPropertyNoHistory(new AccessorProperty(property, bindTo)); 368 } 369 370 // Get a logical slot index for a property, with spill slot 0 starting at fieldMaximum. 371 private int logicalSlotIndex(final Property property) { 372 final int slot = property.getSlot(); 373 if (slot < 0) { 374 return -1; 375 } 376 return property.isSpill() ? slot + fieldMaximum : slot; 377 } 378 379 // Update boundaries and flags after a property has been added 380 private void updateFlagsAndBoundaries(final Property newProperty) { 381 if(newProperty.isSpill()) { 382 spillLength = Math.max(spillLength, newProperty.getSlot() + 1); 383 } else { 384 fieldCount = Math.max(fieldCount, newProperty.getSlot() + 1); 385 } 386 if (isValidArrayIndex(getArrayIndex(newProperty.getKey()))) { 387 setContainsArrayKeys(); 388 } 389 } 390 391 // Update the free slots bitmap for a property that has been deleted and/or added. This method is not synchronized 392 // as it is always invoked on a newly created instance. 393 private void updateFreeSlots(final Property oldProperty, final Property newProperty) { 394 // Free slots bitset is possibly shared with parent map, so we must clone it before making modifications. 395 boolean freeSlotsCloned = false; 396 if (oldProperty != null) { 397 final int slotIndex = logicalSlotIndex(oldProperty); 398 if (slotIndex >= 0) { 399 final BitSet newFreeSlots = freeSlots == null ? new BitSet() : (BitSet)freeSlots.clone(); 400 assert !newFreeSlots.get(slotIndex); 401 newFreeSlots.set(slotIndex); 402 freeSlots = newFreeSlots; 403 freeSlotsCloned = true; 404 } 405 } 406 if (freeSlots != null && newProperty != null) { 407 final int slotIndex = logicalSlotIndex(newProperty); 408 if (slotIndex > -1 && freeSlots.get(slotIndex)) { 409 final BitSet newFreeSlots = freeSlotsCloned ? freeSlots : ((BitSet)freeSlots.clone()); 410 newFreeSlots.clear(slotIndex); 411 freeSlots = newFreeSlots.isEmpty() ? null : newFreeSlots; 412 } 413 } 414 } 415 416 /** 417 * Add a property to the map without adding it to the history. This should be used for properties that 418 * can't be shared such as bound properties, or properties that are expected to be added only once. 419 * 420 * @param property {@link Property} being added. 421 * @return New {@link PropertyMap} with {@link Property} added. 422 */ 423 public PropertyMap addPropertyNoHistory(final Property property) { 424 if (listeners != null) { 425 listeners.propertyAdded(property); 426 } 427 final PropertyHashMap newProperties = properties.immutableAdd(property); 428 final PropertyMap newMap = new PropertyMap(this, newProperties); 429 newMap.updateFlagsAndBoundaries(property); 430 newMap.updateFreeSlots(null, property); 431 432 return newMap; 433 } 434 435 /** 436 * Add a property to the map. Cloning or using an existing map if available. 437 * 438 * @param property {@link Property} being added. 439 * 440 * @return New {@link PropertyMap} with {@link Property} added. 441 */ 442 public synchronized PropertyMap addProperty(final Property property) { 443 if (listeners != null) { 444 listeners.propertyAdded(property); 445 } 446 PropertyMap newMap = checkHistory(property); 447 448 if (newMap == null) { 449 final PropertyHashMap newProperties = properties.immutableAdd(property); 450 newMap = new PropertyMap(this, newProperties); 451 newMap.updateFlagsAndBoundaries(property); 452 newMap.updateFreeSlots(null, property); 453 addToHistory(property, newMap); 454 } 455 456 return newMap; 457 } 458 459 /** 460 * Remove a property from a map. Cloning or using an existing map if available. 461 * 462 * @param property {@link Property} being removed. 463 * 464 * @return New {@link PropertyMap} with {@link Property} removed or {@code null} if not found. 465 */ 466 public synchronized PropertyMap deleteProperty(final Property property) { 467 if (listeners != null) { 468 listeners.propertyDeleted(property); 469 } 470 PropertyMap newMap = checkHistory(property); 471 final String key = property.getKey(); 472 473 if (newMap == null && properties.containsKey(key)) { 474 final PropertyHashMap newProperties = properties.immutableRemove(key); 475 final boolean isSpill = property.isSpill(); 476 final int slot = property.getSlot(); 477 // If deleted property was last field or spill slot we can make it reusable by reducing field/slot count. 478 // Otherwise mark it as free in free slots bitset. 479 if (isSpill && slot >= 0 && slot == spillLength - 1) { 480 newMap = new PropertyMap(newProperties, className, fieldCount, fieldMaximum, spillLength - 1, containsArrayKeys()); 481 newMap.freeSlots = freeSlots; 482 } else if (!isSpill && slot >= 0 && slot == fieldCount - 1) { 483 newMap = new PropertyMap(newProperties, className, fieldCount - 1, fieldMaximum, spillLength, containsArrayKeys()); 484 newMap.freeSlots = freeSlots; 485 } else { 486 newMap = new PropertyMap(this, newProperties); 487 newMap.updateFreeSlots(property, null); 488 } 489 addToHistory(property, newMap); 490 } 491 492 return newMap; 493 } 494 495 /** 496 * Replace an existing property with a new one. 497 * 498 * @param oldProperty Property to replace. 499 * @param newProperty New {@link Property}. 500 * 501 * @return New {@link PropertyMap} with {@link Property} replaced. 502 */ 503 public PropertyMap replaceProperty(final Property oldProperty, final Property newProperty) { 504 if (listeners != null) { 505 listeners.propertyModified(oldProperty, newProperty); 506 } 507 // Add replaces existing property. 508 final PropertyHashMap newProperties = properties.immutableReplace(oldProperty, newProperty); 509 final PropertyMap newMap = new PropertyMap(this, newProperties); 510 /* 511 * See ScriptObject.modifyProperty and ScriptObject.setUserAccessors methods. 512 * 513 * This replaceProperty method is called only for the following three cases: 514 * 515 * 1. To change flags OR TYPE of an old (cloned) property. We use the same spill slots. 516 * 2. To change one UserAccessor property with another - user getter or setter changed via 517 * Object.defineProperty function. Again, same spill slots are re-used. 518 * 3. Via ScriptObject.setUserAccessors method to set user getter and setter functions 519 * replacing the dummy AccessorProperty with null method handles (added during map init). 520 * 521 * In case (1) and case(2), the property type of old and new property is same. For case (3), 522 * the old property is an AccessorProperty and the new one is a UserAccessorProperty property. 523 */ 524 525 final boolean sameType = oldProperty.getClass() == newProperty.getClass(); 526 assert sameType || 527 oldProperty instanceof AccessorProperty && 528 newProperty instanceof UserAccessorProperty : 529 "arbitrary replaceProperty attempted " + sameType + " oldProperty=" + oldProperty.getClass() + " newProperty=" + newProperty.getClass() + " [" + oldProperty.getLocalType() + " => " + newProperty.getLocalType() + "]"; 530 531 newMap.flags = flags; 532 533 /* 534 * spillLength remains same in case (1) and (2) because of slot reuse. Only for case (3), we need 535 * to add spill count of the newly added UserAccessorProperty property. 536 */ 537 if (!sameType) { 538 newMap.spillLength = Math.max(spillLength, newProperty.getSlot() + 1); 539 newMap.updateFreeSlots(oldProperty, newProperty); 540 } 541 return newMap; 542 } 543 544 /** 545 * Make a new UserAccessorProperty property. getter and setter functions are stored in 546 * this ScriptObject and slot values are used in property object. Note that slots 547 * are assigned speculatively and should be added to map before adding other 548 * properties. 549 * 550 * @param key the property name 551 * @param propertyFlags attribute flags of the property 552 * @return the newly created UserAccessorProperty 553 */ 554 public UserAccessorProperty newUserAccessors(final String key, final int propertyFlags) { 555 return new UserAccessorProperty(key, propertyFlags, getFreeSpillSlot()); 556 } 557 558 /** 559 * Find a property in the map. 560 * 561 * @param key Key to search for. 562 * 563 * @return {@link Property} matching key. 564 */ 565 public Property findProperty(final String key) { 566 return properties.find(key); 567 } 568 569 /** 570 * Adds all map properties from another map. 571 * 572 * @param other The source of properties. 573 * 574 * @return New {@link PropertyMap} with added properties. 575 */ 576 public PropertyMap addAll(final PropertyMap other) { 577 assert this != other : "adding property map to itself"; 578 final Property[] otherProperties = other.properties.getProperties(); 579 final PropertyHashMap newProperties = properties.immutableAdd(otherProperties); 580 581 final PropertyMap newMap = new PropertyMap(this, newProperties); 582 for (final Property property : otherProperties) { 583 // This method is only safe to use with non-slotted, native getter/setter properties 584 assert property.getSlot() == -1; 585 assert !(isValidArrayIndex(getArrayIndex(property.getKey()))); 586 } 587 588 return newMap; 589 } 590 591 /** 592 * Return an array of all properties. 593 * 594 * @return Properties as an array. 595 */ 596 public Property[] getProperties() { 597 return properties.getProperties(); 598 } 599 600 /** 601 * Return the name of the class of objects using this property map. 602 * 603 * @return class name of owner objects. 604 */ 605 public String getClassName() { 606 return className; 607 } 608 609 /** 610 * Prevents the map from having additional properties. 611 * 612 * @return New map with {@link #NOT_EXTENSIBLE} flag set. 613 */ 614 PropertyMap preventExtensions() { 615 final PropertyMap newMap = new PropertyMap(this); 616 newMap.flags |= NOT_EXTENSIBLE; 617 return newMap; 618 } 619 620 /** 621 * Prevents properties in map from being modified. 622 * 623 * @return New map with {@link #NOT_EXTENSIBLE} flag set and properties with 624 * {@link Property#NOT_CONFIGURABLE} set. 625 */ 626 PropertyMap seal() { 627 PropertyHashMap newProperties = EMPTY_HASHMAP; 628 629 for (final Property oldProperty : properties.getProperties()) { 630 newProperties = newProperties.immutableAdd(oldProperty.addFlags(Property.NOT_CONFIGURABLE)); 631 } 632 633 final PropertyMap newMap = new PropertyMap(this, newProperties); 634 newMap.flags |= NOT_EXTENSIBLE; 635 636 return newMap; 637 } 638 639 /** 640 * Prevents properties in map from being modified or written to. 641 * 642 * @return New map with {@link #NOT_EXTENSIBLE} flag set and properties with 643 * {@link Property#NOT_CONFIGURABLE} and {@link Property#NOT_WRITABLE} set. 644 */ 645 PropertyMap freeze() { 646 PropertyHashMap newProperties = EMPTY_HASHMAP; 647 648 for (final Property oldProperty : properties.getProperties()) { 649 int propertyFlags = Property.NOT_CONFIGURABLE; 650 651 if (!(oldProperty instanceof UserAccessorProperty)) { 652 propertyFlags |= Property.NOT_WRITABLE; 653 } 654 655 newProperties = newProperties.immutableAdd(oldProperty.addFlags(propertyFlags)); 656 } 657 658 final PropertyMap newMap = new PropertyMap(this, newProperties); 659 newMap.flags |= NOT_EXTENSIBLE; 660 661 return newMap; 662 } 663 664 /** 665 * Check for any configurable properties. 666 * 667 * @return {@code true} if any configurable. 668 */ 669 private boolean anyConfigurable() { 670 for (final Property property : properties.getProperties()) { 671 if (property.isConfigurable()) { 672 return true; 673 } 674 } 675 676 return false; 677 } 678 679 /** 680 * Check if all properties are frozen. 681 * 682 * @return {@code true} if all are frozen. 683 */ 684 private boolean allFrozen() { 685 for (final Property property : properties.getProperties()) { 686 // check if it is a data descriptor 687 if (!(property instanceof UserAccessorProperty)) { 688 if (property.isWritable()) { 689 return false; 690 } 691 } 692 if (property.isConfigurable()) { 693 return false; 694 } 695 } 696 697 return true; 698 } 699 700 /** 701 * Check prototype history for an existing property map with specified prototype. 702 * 703 * @param proto New prototype object. 704 * 705 * @return Existing {@link PropertyMap} or {@code null} if not found. 706 */ 707 private PropertyMap checkProtoHistory(final ScriptObject proto) { 708 final PropertyMap cachedMap; 709 if (protoHistory != null) { 710 final SoftReference<PropertyMap> weakMap = protoHistory.get(proto); 711 cachedMap = (weakMap != null ? weakMap.get() : null); 712 } else { 713 cachedMap = null; 714 } 715 716 if (Context.DEBUG && cachedMap != null) { 717 protoHistoryHit.increment(); 718 } 719 720 return cachedMap; 721 } 722 723 /** 724 * Add a map to the prototype history. 725 * 726 * @param newProto Prototype to add (key.) 727 * @param newMap {@link PropertyMap} associated with prototype. 728 */ 729 private void addToProtoHistory(final ScriptObject newProto, final PropertyMap newMap) { 730 if (protoHistory == null) { 731 protoHistory = new WeakHashMap<>(); 732 } 733 734 protoHistory.put(newProto, new SoftReference<>(newMap)); 735 } 736 737 /** 738 * Track the modification of the map. 739 * 740 * @param property Mapping property. 741 * @param newMap Modified {@link PropertyMap}. 742 */ 743 private void addToHistory(final Property property, final PropertyMap newMap) { 744 if (history == null) { 745 history = new WeakHashMap<>(); 746 } 747 748 history.put(property, new SoftReference<>(newMap)); 749 } 750 751 /** 752 * Check the history for a map that already has the given property added. 753 * 754 * @param property {@link Property} to add. 755 * 756 * @return Existing map or {@code null} if not found. 757 */ 758 private PropertyMap checkHistory(final Property property) { 759 760 if (history != null) { 761 final SoftReference<PropertyMap> ref = history.get(property); 762 final PropertyMap historicMap = ref == null ? null : ref.get(); 763 764 if (historicMap != null) { 765 if (Context.DEBUG) { 766 historyHit.increment(); 767 } 768 769 return historicMap; 770 } 771 } 772 773 return null; 774 } 775 776 /** 777 * Returns true if the two maps have identical properties in the same order, but allows the properties to differ in 778 * their types. This method is mostly useful for tests. 779 * @param otherMap the other map 780 * @return true if this map has identical properties in the same order as the other map, allowing the properties to 781 * differ in type. 782 */ 783 public boolean equalsWithoutType(final PropertyMap otherMap) { 784 if (properties.size() != otherMap.properties.size()) { 785 return false; 786 } 787 788 final Iterator<Property> iter = properties.values().iterator(); 789 final Iterator<Property> otherIter = otherMap.properties.values().iterator(); 790 791 while (iter.hasNext() && otherIter.hasNext()) { 792 if (!iter.next().equalsWithoutType(otherIter.next())) { 793 return false; 794 } 795 } 796 797 return true; 798 } 799 800 @Override 801 public String toString() { 802 final StringBuilder sb = new StringBuilder(); 803 804 sb.append(Debug.id(this)); 805 sb.append(" = {\n"); 806 807 for (final Property property : getProperties()) { 808 sb.append('\t'); 809 sb.append(property); 810 sb.append('\n'); 811 } 812 813 sb.append('}'); 814 815 return sb.toString(); 816 } 817 818 @Override 819 public Iterator<Object> iterator() { 820 return new PropertyMapIterator(this); 821 } 822 823 /** 824 * Check if this map contains properties with valid array keys 825 * 826 * @return {@code true} if this map contains properties with valid array keys 827 */ 828 public final boolean containsArrayKeys() { 829 return (flags & CONTAINS_ARRAY_KEYS) != 0; 830 } 831 832 /** 833 * Flag this object as having array keys in defined properties 834 */ 835 private void setContainsArrayKeys() { 836 flags |= CONTAINS_ARRAY_KEYS; 837 } 838 839 /** 840 * Test to see if {@link PropertyMap} is extensible. 841 * 842 * @return {@code true} if {@link PropertyMap} can be added to. 843 */ 844 boolean isExtensible() { 845 return (flags & NOT_EXTENSIBLE) == 0; 846 } 847 848 /** 849 * Test to see if {@link PropertyMap} is not extensible or any properties 850 * can not be modified. 851 * 852 * @return {@code true} if {@link PropertyMap} is sealed. 853 */ 854 boolean isSealed() { 855 return !isExtensible() && !anyConfigurable(); 856 } 857 858 /** 859 * Test to see if {@link PropertyMap} is not extensible or all properties 860 * can not be modified. 861 * 862 * @return {@code true} if {@link PropertyMap} is frozen. 863 */ 864 boolean isFrozen() { 865 return !isExtensible() && allFrozen(); 866 } 867 868 /** 869 * Return a free field slot for this map, or {@code -1} if none is available. 870 * 871 * @return free field slot or -1 872 */ 873 int getFreeFieldSlot() { 874 if (freeSlots != null) { 875 final int freeSlot = freeSlots.nextSetBit(0); 876 if (freeSlot > -1 && freeSlot < fieldMaximum) { 877 return freeSlot; 878 } 879 } 880 if (fieldCount < fieldMaximum) { 881 return fieldCount; 882 } 883 return -1; 884 } 885 886 /** 887 * Get a free spill slot for this map. 888 * 889 * @return free spill slot 890 */ 891 int getFreeSpillSlot() { 892 if (freeSlots != null) { 893 final int freeSlot = freeSlots.nextSetBit(fieldMaximum); 894 if (freeSlot > -1) { 895 return freeSlot - fieldMaximum; 896 } 897 } 898 return spillLength; 899 } 900 901 /** 902 * Return a property map with the same layout that is associated with the new prototype object. 903 * 904 * @param newProto New prototype object to replace oldProto. 905 * @return New {@link PropertyMap} with prototype changed. 906 */ 907 public synchronized PropertyMap changeProto(final ScriptObject newProto) { 908 final PropertyMap nextMap = checkProtoHistory(newProto); 909 if (nextMap != null) { 910 return nextMap; 911 } 912 913 if (Context.DEBUG) { 914 setProtoNewMapCount.increment(); 915 } 916 917 final PropertyMap newMap = new PropertyMap(this); 918 addToProtoHistory(newProto, newMap); 919 920 return newMap; 921 } 922 923 924 /** 925 * {@link PropertyMap} iterator. 926 */ 927 private static class PropertyMapIterator implements Iterator<Object> { 928 /** Property iterator. */ 929 final Iterator<Property> iter; 930 931 /** Current Property. */ 932 Property property; 933 934 /** 935 * Constructor. 936 * 937 * @param propertyMap {@link PropertyMap} to iterate over. 938 */ 939 PropertyMapIterator(final PropertyMap propertyMap) { 940 iter = Arrays.asList(propertyMap.properties.getProperties()).iterator(); 941 property = iter.hasNext() ? iter.next() : null; 942 skipNotEnumerable(); 943 } 944 945 /** 946 * Ignore properties that are not enumerable. 947 */ 948 private void skipNotEnumerable() { 949 while (property != null && !property.isEnumerable()) { 950 property = iter.hasNext() ? iter.next() : null; 951 } 952 } 953 954 @Override 955 public boolean hasNext() { 956 return property != null; 957 } 958 959 @Override 960 public Object next() { 961 if (property == null) { 962 throw new NoSuchElementException(); 963 } 964 965 final Object key = property.getKey(); 966 property = iter.next(); 967 skipNotEnumerable(); 968 969 return key; 970 } 971 972 @Override 973 public void remove() { 974 throw new UnsupportedOperationException("remove"); 975 } 976 } 977 978 /* 979 * Debugging and statistics. 980 */ 981 982 /** 983 * Debug helper function that returns the diff of two property maps, only 984 * displaying the information that is different and in which map it exists 985 * compared to the other map. Can be used to e.g. debug map guards and 986 * investigate why they fail, causing relink 987 * 988 * @param map0 the first property map 989 * @param map1 the second property map 990 * 991 * @return property map diff as string 992 */ 993 public static String diff(final PropertyMap map0, final PropertyMap map1) { 994 final StringBuilder sb = new StringBuilder(); 995 996 if (map0 != map1) { 997 sb.append(">>> START: Map diff"); 998 boolean found = false; 999 1000 for (final Property p : map0.getProperties()) { 1001 final Property p2 = map1.findProperty(p.getKey()); 1002 if (p2 == null) { 1003 sb.append("FIRST ONLY : [").append(p).append("]"); 1004 found = true; 1005 } else if (p2 != p) { 1006 sb.append("DIFFERENT : [").append(p).append("] != [").append(p2).append("]"); 1007 found = true; 1008 } 1009 } 1010 1011 for (final Property p2 : map1.getProperties()) { 1012 final Property p1 = map0.findProperty(p2.getKey()); 1013 if (p1 == null) { 1014 sb.append("SECOND ONLY: [").append(p2).append("]"); 1015 found = true; 1016 } 1017 } 1018 1019 //assert found; 1020 1021 if (!found) { 1022 sb.append(map0). 1023 append("!="). 1024 append(map1); 1025 } 1026 1027 sb.append("<<< END: Map diff\n"); 1028 } 1029 1030 return sb.toString(); 1031 } 1032 1033 // counters updated only in debug mode 1034 private static LongAdder count; 1035 private static LongAdder clonedCount; 1036 private static LongAdder historyHit; 1037 private static LongAdder protoInvalidations; 1038 private static LongAdder protoHistoryHit; 1039 private static LongAdder setProtoNewMapCount; 1040 static { 1041 if (Context.DEBUG) { 1042 count = new LongAdder(); 1043 clonedCount = new LongAdder(); 1044 historyHit = new LongAdder(); 1045 protoInvalidations = new LongAdder(); 1046 protoHistoryHit = new LongAdder(); 1047 setProtoNewMapCount = new LongAdder(); 1048 } 1049 } 1050 1051 /** 1052 * @return Total number of maps. 1053 */ 1054 public static long getCount() { 1055 return count.longValue(); 1056 } 1057 1058 /** 1059 * @return The number of maps that were cloned. 1060 */ 1061 public static long getClonedCount() { 1062 return clonedCount.longValue(); 1063 } 1064 1065 /** 1066 * @return The number of times history was successfully used. 1067 */ 1068 public static long getHistoryHit() { 1069 return historyHit.longValue(); 1070 } 1071 1072 /** 1073 * @return The number of times prototype changes caused invalidation. 1074 */ 1075 public static long getProtoInvalidations() { 1076 return protoInvalidations.longValue(); 1077 } 1078 1079 /** 1080 * @return The number of times proto history was successfully used. 1081 */ 1082 public static long getProtoHistoryHit() { 1083 return protoHistoryHit.longValue(); 1084 } 1085 1086 /** 1087 * @return The number of times prototypes were modified. 1088 */ 1089 public static long getSetProtoNewMapCount() { 1090 return setProtoNewMapCount.longValue(); 1091 } 1092} 1093