Skip to content

fix ClassCache#get(...) and ClassCache.CacheKey#hashCode()#1886

Merged
gbrail merged 3 commits intomozilla:masterfrom
ZZZank:classcache-fix
May 5, 2025
Merged

fix ClassCache#get(...) and ClassCache.CacheKey#hashCode()#1886
gbrail merged 3 commits intomozilla:masterfrom
ZZZank:classcache-fix

Conversation

@ZZZank
Copy link
Copy Markdown
Contributor

@ZZZank ZZZank commented Apr 23, 2025

  • The original hashing of CacheKey will completely ignore the cls field when sec is present, now it will combine hash code for both fields.
  • ClassCache.get(...) will now try to associate a new ClassCache object when none was found, then return the new object, as described in @return javadoc of it.

Fixes #1884

int result = cls.hashCode();
if (sec != null) {
result = sec.hashCode() * 31;
result = result * 31 + sec.hashCode();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any knowledge / data about the "* 31" bit, which I assume was added long ago in history. It doesn't do anything to make this solution more correct -- if anything, it might make the hash table behave worse! Why don't we take it out and just add (or OR) the two values together?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

31 is a "magic number" used by many simple hash implementations, and I believe the * 31 part is just simulating Arrays.hashCode(...) behaviour, which will multiply the intermediate result by 31 before adding hashcode from the next array element.

In context here, yes, * 31 does look redundant, but I personally prefer XOR over add or logical add (aka OR), because I (somehow) believe it does better in mixing bits.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple xoring for combining hashes isn’t great as two similar hashes will end up zeroing a lot of bits, and gives the same hash for A, B as for B, A. Multiplying by a small prime tends to spread a set of bits out over the available space and keeps this good for the mod N operation you’ll commonly end up using to choose a bucket in the table.

There are better / faster hashing functions out there, but they often need a final mixing steps so aren’t so well suited to hashCode() methods, and I don’t know how well they would combine with prime multiply and add hash codes.

@rPraml
Copy link
Copy Markdown
Contributor

rPraml commented Apr 27, 2025

@ZZZank thanks for discovering and fixing the hashcode problem. I probably implemented this wrong back then

The good thing about this:

  • at least the equals was correct. So it will not cause any failures, but we might have many hash collisions.
  • Nearly no one uses securityContext (and in my opinion the SecurityContext support should be removed from Rhino because SecurityManager etc. is deprecated EXPERIMENTAL: Remove SecurityManager #1353)

Do we have any knowledge / data about the "* 31" bit,

It is the default implementation in many cases. (JDK, autogenereated by many IDEs)
Multiplying by a prime number should distribute the bits evenly.
There is also 92821 as recommended prime...
but here, I think also XORing should be OK

"top scope have no associated ClassCache and cannot have ClassCache associated due to not being a ScriptableObject");
}
cache = new ClassCache();
cache.associate(((ScriptableObject) topScope));
Copy link
Copy Markdown
Contributor

@rPraml rPraml Apr 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ZZZank Have you encountered a problem with the RuntimeException? Or have you simply found that the documentation doesn't match the implementation?

The fix seems to be not thread safe. If two threads tries to init the cache, you might have a race condition here.
At least in our application, we share one topLevelScope for multiple threads (and this scope gets a ClassCache assigned on construction)

Normally, when you use initStandardObjects on the top scope, a ClassCache should be added automatically. If you use "initSafeStandardObjects" (which means I don't want to use LiveConnect) it would be totally OK, if there is no classCache and I get the exception, because it means, that some code will try to access LiveConnect, that should not (and may be tries to break out the sandbox)

In my opinion, instead of implementing a possible non-threadsafe lazy init, the documentation should therefore be adapted, even if this is strictly speaking a change to the API.

BTW: The implementation was changed here: e59e568#diff-69ef3670c4b5abfaf14ea2f9571184d5193995fbea54dab635d3daff3f06c06b but the javadoc was not adjusted

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This problem was found when I was trying to use ClassCache#get(...) inside FunctionObject#<init>(...), in which case MozillaSuiteTest will fail. There're 4876 failed MozillaSuiteTest in total, the first of them is:

runMozillaTest[0, js=testsrc/tests/bigo-test.js, opt=false]

java.lang.RuntimeException: Can't find top level scope for ClassCache.get
	at org.mozilla.javascript.ClassCache.get(ClassCache.java:84)
	at org.mozilla.javascript.FunctionObject.<init>(FunctionObject.java:84)
	at org.mozilla.javascript.ScriptableObject.defineFunctionProperties(ScriptableObject.java:2101)
	at org.mozilla.javascript.drivers.ShellTest.runNoFork(ShellTest.java:434)
	at org.mozilla.javascript.tests.MozillaSuiteTest.runMozillaTest(MozillaSuiteTest.java:197)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)

@gbrail
Copy link
Copy Markdown
Collaborator

gbrail commented May 5, 2025

OK, thanks -- I get the "+31 thing" in that context. Thanks for doing this!

@gbrail
Copy link
Copy Markdown
Collaborator

gbrail commented May 5, 2025

Anyway, I'm OK with the XOR for now seeing that we have a few different opinions and we can always test or adjust later if we need.

@gbrail gbrail merged commit 050d51f into mozilla:master May 5, 2025
3 checks passed
@ZZZank ZZZank deleted the classcache-fix branch January 5, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ClassCache#get(Scriptable) behaviour inconsistent with the behaviour described in Javadoc

4 participants