-
Notifications
You must be signed in to change notification settings - Fork 38
add fix to identityhashmap, add one test class for it #212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add fix to identityhashmap, add one test class for it #212
Conversation
darko-marinov
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution. My change requests are mostly minor.
| } | ||
|
|
||
| public void addRemove() { | ||
| MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC, "remove", "()V", null, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How did you write this code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I simply follow each line in the original remove method and convert each line into asm visitor by referring to existing NonDex code like addHasNext methods. The one part I had to find other reference and ask LLM for help is the Complex condition part, which requires some complex jump instructions. But overall the syntax are taken from other existing part in the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, sorry we didn't tell you about ASMifier https://asm.ow2.io/faq.html#Q10
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@EdwinIngJ did tell me about the tool, I used it to verify my instructions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tested your fix across multiple Java versions (for example, Java 8)? In some cases, the implementation can differ between versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please double check java -version. If you're running on your VM, please email me the details so I can try out myself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I was accidentally using the Oracle JDK 25 instead of OpenJDK 25. Now the build and test works fine. @EdwinIngJ Could you tell me more about the failure with Java 11? I tested in all above Java version including Java 11 and it worked fine.
My Java 11 JDK:
chihful2@fa25-cs527-009:~/forks/NonDex$ java --version
openjdk 11.0.29 2025-10-21
OpenJDK Runtime Environment (build 11.0.29+7-post-Ubuntu-1ubuntu124.04)
OpenJDK 64-Bit Server VM (build 11.0.29+7-post-Ubuntu-1ubuntu124.04, mixed mode, sharing)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get the error running the integration tests:
[INFO] Building: comprehensive/pom.xml
[INFO] run post-build script verify.groovy
[INFO] The build exited with code 1. See /home/edwinji2/temp/NonDex/nondex-maven-plugin/target/it/comprehensive/build.log for details.
[INFO] comprehensive/pom.xml ............................ FAILED (17.35 s)
[INFO] Running edu.illinois.nondex.core.MapTest
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.001 s <<< FAILURE! - in edu.illinois.nondex.core.MapTest
[ERROR] testRemove[2](edu.illinois.nondex.core.MapTest) Time elapsed: 0 s <<< FAILURE!
java.lang.AssertionError
at edu.illinois.nondex.core.MapTest.testRemove(MapTest.java:141)
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.009 s <<< FAILURE! - in edu.illinois.nondex.core.MapTest
[ERROR] testRemove[2](edu.illinois.nondex.core.MapTest) Time elapsed: 0 s <<< FAILURE!
java.lang.AssertionError
at edu.illinois.nondex.core.MapTest.testRemove(MapTest.java:141)
[INFO]
[INFO] Results:
[INFO]
[ERROR] Failures:
[ERROR] edu.illinois.nondex.core.MapTest.testRemove[2](edu.illinois.nondex.core.MapTest)
[ERROR] Run 1: MapTest.testRemove:141
[ERROR] Run 2: MapTest.testRemove:141
[ERROR] Run 3: MapTest.testRemove:141
Here is my Java version:
edwinji2@fa25-cs527-004:~/temp/NonDex$ java -version
openjdk version "11.0.29" 2025-10-21
OpenJDK Runtime Environment (build 11.0.29+7-post-Ubuntu-1ubuntu124.04)
OpenJDK 64-Bit Server VM (build 11.0.29+7-post-Ubuntu-1ubuntu124.04, mixed mode, sharing)
edwinji2@fa25-cs527-004:~/temp/NonDex$ javac -version
javac 11.0.29
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still couldn't reproduce the error myself. Running the integration tests with mvn clean install and running mvn clean verify -pl nondex-maven-plugin afterwards all passed for me. The MapTest in integration test should works exactly the same as the test in nondex-test module right? Is there a clean way to run the test possibly with different seeds like nondexRuns?
My integration tests result:
[INFO] --- invoker:3.9.1:integration-test (integration-test) @ nondex-maven-plugin ---
[INFO] Building: failing-it/pom.xml
[INFO] run post-build script verify.groovy
[INFO] failing-it/pom.xml ............................... SUCCESS (11.36 s)
[INFO] Building: simple-multimodule-it/pom.xml
[INFO] run post-build script verify.groovy
[INFO] simple-multimodule-it/pom.xml .................... SUCCESS (11.43 s)
[INFO] Building: clean-it/pom.xml
[INFO] run post-build script verify.groovy
[INFO] clean-it/pom.xml ................................. SUCCESS (8.756 s)
[INFO] Building: comprehensive/pom.xml
[INFO] run post-build script verify.groovy
[INFO] comprehensive/pom.xml ............................ SUCCESS (14.43 s)
[INFO] Building: simple-it/pom.xml
[INFO] run post-build script verify.groovy
[INFO] simple-it/pom.xml ................................ SUCCESS (9.554 s)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I also ran the same commands. I'm not aware of a clean way of specifying the seed in the test. But Github action was recently added to master. Try rebasing on top of master and let the Github action run and maybe that will provide more insight.
| /* | ||
| The MIT License (MIT) | ||
| Copyright (c) 2015 Alex Gyori | ||
| Copyright (c) 2022 Kaiyao Ke |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add your name here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The checkstyle enforces fixed header so I couldn't add my name in it. I added my name in comment above the test case instead, is this ok?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine as you did it, although we/you could have changed the Checkstyle config.
|
|
||
| @Test | ||
| public void testRemoveAllArrayIndexOutOfBounds() { | ||
| for (int i = 0; i < 10_000; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you really need 10K iterations? Would 100 or even fewer suffice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess 1000 iteration is enough to make the original code fail 95% of the time. 100 is probably not enough for it to fail consistently. Should I make it 1000?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does it fail so rarely? Does it require some special condition among the three keys, e.g., that their System.identifyHashCode be equal? What if you put more than three keys?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is indeed related to hash because the hash decides where the keys are placed in the underlying linear table. As the comment in JDK says:
// it must catch the rare case where an element already
// seen is swapped into a vacant slot that will be later
// traversed by this iterator. We cannot allow future
// next() calls to return it again. The likelihood of
// this occurring under 2/3 load factor is very slim, but
// when it does happen, we must make a copy of the rest of
// the table to use for the rest of the traversal. Since
// this can only happen when we are near the end of the table,
// even in these rare cases, this is not very expensive in
// time or space.
I believe this block (which is removed in my fix) only triggers when the deleted key is in later part of the table and also the other iterated keys are place in according positions. It is even rarer since I had only 3 elements, as it might be triggered more often when close to capacity of table.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it trigger more often if you have a map with a smaller capacity, e.g., new IdentityHashMap<>(3), or put more than three elements in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I set the capacity to 4, and now the original removeAll fails 95% of the time with 100 iterations. I have committed the change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for changing the initial capacity. Can we increase 100 iterations to 999 so that the failure goes from 95% to, hopefully, 99+%?
| map.put(k2, 2); | ||
| map.put(k3, 3); | ||
|
|
||
| map.keySet().removeAll(Collections.singleton(k2)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there some assertion to check (e.g., that the size of map is two after this), or are you simply checking that this doesn't throw an exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I am simply checking that this doesn't throw an exception. I consider it redundant to add additional assertions because most map behaviors are already checked by tests in MapTest, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, fine to not have an assertion.
| map.keySet().removeAll(Collections.singleton(k2)); | ||
| } | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add newline at the end.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem
|
@EdwinIngJ and @cperkkk could you take a quick look at this PR? |
|
Pushed the commit fixing access problem in Java 8. Tested the fix on Java 8, Java 17, Java 21. |
c29f8a8 to
be3ff10
Compare
|
We're getting close to the deadline, so please squash your four commit into one, and I can accept the PR for now. We can (partially) revert it later if it does break something. |
|
Note that all the checks have passed for Java 8/11/17/21/25 on Maven and Java 8/11/17/21 on Gradle. |
add author comment and newline minimize table size to increase failing probability for removeAll Fixed incompatibility with Java 8 by inlining outer class methods increase test iteration number
be3ff10 to
90f3d5e
Compare
|
Squashed all commits. All above problems seem to have been resolved. Feel free to contact me if it does break something afterwards! |
darko-marinov
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's accept for now (and revert later if needed). Thank you @lycoris106 and @EdwinIngJ for working on this until the last hour!
Resolves issue #209, and adds a corresponding test class
IdentityHashMapTest.javaFix
The fix follows the proposed fix in the issue, by removing the block:
The purpose of this block is to avoid revisiting keys after some keys are moved due to the
closeDeletionprocess. However, thiscloseDeletionprocess would not affect nondex's version of iterationnextIndex()since it always loops through the whole table to find the key.Also, another block in the
removemethod is removed to make our instrumentation simpler:This block can also be safely removed because its purpose is to deal with the array-cutoff situation which only happens in the first block we removed.
Fix implementation
The implementation follow NonDex's practice of writing asm visitors to instrument bytecode. Just like
addNextIndex, theaddRemovemethod rewrites a newremove()for the iterator class. The originalremove()is renamed intooriginalRemove(though we can choose to simply overwrite it since it's not used anymore).Added Test Class
A test class
IdentityHashMapTestis added intonondex-testmodule to specifically test theremoveAllbehavior. I chose to write a new test class since this test shouldn't be parameterized likeMapTest. Since onlyIdentityHashMap'sremoveAlluses==to compare elements, the test wouldn't work for othermaps.The purpose of the test is to make sure the
removeAllmethod succeeds without throwingArrayIndexOutOfBoundsExceptiondue to the bug.