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