Can't get Multithreaded Nashorn uses to Scale
Hannes Wallnöfer
hannes.wallnoefer at oracle.com
Wed Dec 7 15:01:27 UTC 2016
Hi Jesus,
I’m trying to reproduce the problem, and just want to make sure I get the missing pieces right.
You already showed us how you’re setting up the engine and the JS code you’re running. I assume the JSON code you’re parsing is a simple array of objects? And you’re just calling Invocable.invokeFunction on the ScriptEngine from multiple threads in parallel, right?
Thanks,
Hannes
> Am 07.12.2016 um 00:03 schrieb Jesus Luzon <jluzon at riotgames.com>:
>
> When we share one invocable across many threads and run invokeFunction it
> happens, such as this:
>
> ExecutorService executor = Executors.newFixedThreadPool(50);
>>
>> Invocable invocable = generateInvocable(script);
>>
>> AtomicLong count = new AtomicLong();
>>
>> for (int i = 0; i < 50; i++) {
>>
>> executor.submit(new Runnable() {
>>
>> @Override
>>
>> public void run() {
>>
>> try {
>>
>> while(true) {
>>
>> invocable.invokeFunction("transform",
>>> something);
>>
>> count.incrementAndGet();
>>
>> }
>>
>> } catch (NoSuchMethodException | ScriptException
>>> e) {
>>
>> e.printStackTrace();
>>
>> }
>>
>> }
>>
>> });
>>
>> }
>>
>>
>
>
> On Tue, Dec 6, 2016 at 2:59 PM, Jim Laskey (Oracle) <james.laskey at oracle.com
>> wrote:
>
>> Intersting. The example you posted demonstrates this behaviour? If so
>> I’ll file a bug and dig in. It sounds like an object is being reused
>> across invocations and accumulating changes to the property map.
>>
>> — Jim
>>
>>
>> On Dec 6, 2016, at 5:12 PM, Jesus Luzon <jluzon at riotgames.com> wrote:
>>
>> With more threads you are impacting the same 8 cores, so it will taper off
>>> after 8 threads. If it’s a 2x4 core machine then I can see 4 being a
>>> threshold depending on System performance. Transport: I meant if you were
>>> using sockets to provide the script.
>>
>> This makes sense. This one's on me then.
>>
>>
>>> So you are using the same invocable instance for all threads? If so,
>>> then you are probably good to go. As far as leaks are concerned, not sure
>>> how you would get leaks from Nashorn. The JSON object is written in Java,
>>> and little JavaScript involved.
>>
>>
>>
>>> In your example, pull up Invocable invocable = generateInvocable(script);
>>> out of the loop and use the same invocable for all threads.
>>
>>
>> We were using one invocable across all threads and we were getting
>> slowdowns on execution, high CPU Usage and memory leaks that led to
>> OutOfMemory errors. I could trace the leak to
>>
>> jdk.nashorn.internal.objects.Global -> *objectSpill* Object[8] ->
>> jdk.nashorn.internal.scripts.JO4 -> *arrayData*
>> jdk.nashorn.internal.runtime.arrays.SparseArraysData -> *underlying*
>> jdk.nashorn.internal.runtime.arrays.DeletedArrayFilter
>>
>> which just keeps growing forever.
>>
>> On Tue, Dec 6, 2016 at 6:30 AM, Jim Laskey (Oracle) <
>> james.laskey at oracle.com> wrote:
>>
>>>
>>> On Dec 6, 2016, at 9:56 AM, Jesus Luzon <jluzon at riotgames.com> wrote:
>>>
>>> The cost of creating a new engine is significant. So share an engine
>>>> across threads but use *eval
>>>> <https://docs.oracle.com/javase/7/docs/api/javax/script/ScriptEngine.html#eval(java.lang.String,%20javax.script.ScriptContext)>*
>>>> (String
>>>> <https://docs.oracle.com/javase/7/docs/api/java/lang/String.html>
>>>> script, ScriptContext
>>>> <https://docs.oracle.com/javase/7/docs/api/javax/script/ScriptContext.html>
>>>> context) instead, separate context per execution. If your JavaScript
>>>> code does not modify globals you can get away with using the same engine,
>>>> same compiled script on each thread.
>>>
>>>
>>> I guess there's a few things here I don't understand. One thing I'm
>>> trying to do is sharing a CompiledScript (which is why I'm using
>>> invocable). Also, what exactly does modify globals mean? All our filters do
>>> the same thing, make a function that takes a JSON String, turns it into a
>>> JSON, modifies it and then stringifies it back. No state is changed of
>>> anything else but there are temporary vars created inside the scope of the
>>> function. When we run this multithreaded, running invokeFunction slows down
>>> significantly and we get crazy memory leaks.
>>>
>>>
>>> So you are using the same invocable instance for all threads? If so,
>>> then you are probably good to go. As far as leaks are concerned, not sure
>>> how you would get leaks from Nashorn. The JSON object is written in Java,
>>> and little JavaScript involved.
>>>
>>>
>>> Of course there are many factors involved n performance. How many cores
>>>> do you have on the test machine? How much memory in the process? What
>>>> transport are you using between threads? That sort of thing. Other than
>>>> constructing then engine and context Nashorn performance should scale.
>>>
>>> I'm using an 8 core machine to test with 2.5Gs of RAM allocated to the
>>> process. Not sure what transports between threads means, but this is the
>>> code I'm benchmarking with. Increasing the number of threads actually makes
>>> it go faster until about 4 threads, then adding more threads takes the same
>>> amount to get to 1000 and and after a certain point it is just slower to
>>> get to 1000 counts. Some of our filters need to be able to run over 1000
>>> times a second (across all threads) and the fastest time I could actually
>>> get with this was about 2.4 seconds for a 1000 counts.
>>>
>>>> ExecutorService executor = Executors.newFixedThreadPool(50);
>>>>
>>>> AtomicLong count = new AtomicLong();
>>>>
>>>> for (int i = 0; i < 50; i++) {
>>>>
>>>> executor.submit(new Runnable() {
>>>>
>>>> @Override
>>>>
>>>> public void run() {
>>>>
>>>>
>>>>> try {
>>>>
>>>> Invocable invocable =
>>>>> generateInvocable(script);
>>>>
>>>> while(true) {
>>>>
>>>> invocable.invokeFunction("transform",
>>>>> something);
>>>>
>>>> count.incrementAndGet();
>>>>
>>>> }
>>>>
>>>> } catch (NoSuchMethodException | ScriptException
>>>>> e) {
>>>>
>>>> e.printStackTrace();
>>>>
>>>> }
>>>>
>>>> }
>>>>
>>>> });
>>>>
>>>> }
>>>>
>>>> long lastTimestamp = System.currentTimeMillis();
>>>>
>>>> while(true) {
>>>>
>>>>
>>>>> if (count.get() > 1000) {
>>>>
>>>> count.getAndAdd(-1000);
>>>>
>>>> System.out.println((System.currentTimeMillis() -
>>>>> lastTimestamp)/1000.0);
>>>>
>>>> lastTimestamp = System.currentTimeMillis();
>>>>
>>>> }
>>>>
>>>> }
>>>>
>>>>
>>> With more threads you are impacting the same 8 cores, so it will taper
>>> off after 8 threads. If it’s a 2x4 core machine then I can see 4 being a
>>> threshold depending on System performance. Transport: I meant if you were
>>> using sockets to provide the script.
>>>
>>> In your example, pull up Invocable invocable = generateInvocable(script);
>>> out of the loop and use the same invocable for all threads.
>>>
>>> - Jim
>>>
>>>
>>>
>>>
>>> On Tue, Dec 6, 2016 at 5:31 AM, Jim Laskey (Oracle) <
>>> james.laskey at oracle.com> wrote:
>>>
>>>>
>>>> On Dec 6, 2016, at 9:19 AM, Jesus Luzon <jluzon at riotgames.com> wrote:
>>>>
>>>> Hey Jim,
>>>>
>>>> I looked at it and I will look into loadWithNewGlobal to see what
>>>> exactly it does since it could be relevant. As for the rest, for my use
>>>> case having threads in the JS would not help. We're using Nashorn to build
>>>> JSON filters in a Dynamic Proxy Service and need any of the threads
>>>> processing a request to be able to execute the script to filter.
>>>>
>>>>
>>>> The cost of creating a new engine is significant. So share an engine
>>>> across threads but use *eval
>>>> <https://docs.oracle.com/javase/7/docs/api/javax/script/ScriptEngine.html#eval(java.lang.String,%20javax.script.ScriptContext)>*
>>>> (String
>>>> <https://docs.oracle.com/javase/7/docs/api/java/lang/String.html>
>>>> script, ScriptContext
>>>> <https://docs.oracle.com/javase/7/docs/api/javax/script/ScriptContext.html>
>>>> context) instead, separate context per execution. If your JavaScript
>>>> code does not modify globals you can get away with using the same engine,
>>>> same compiled script on each thread.
>>>>
>>>>
>>>> Also, when you say a new engine per threads is the worst case what
>>>> exactly do you mean? I would expect an initial cost of compiling the script
>>>> on each thread and then each engine should be able to do its own thing, but
>>>> what I'm seeing is that when running with more than 10 threads all my
>>>> engines get slow at executing code, even though they are all completely
>>>> separate from each other.
>>>>
>>>>
>>>> Of course there are many factors involved n performance. How many cores
>>>> do you have on the test machine? How much memory in the process? What
>>>> transport are you using between threads? That sort of thing. Other than
>>>> constructing then engine and context Nashorn performance should scale.
>>>>
>>>>
>>>> On Tue, Dec 6, 2016 at 5:07 AM, Jim Laskey (Oracle) <
>>>> james.laskey at oracle.com> wrote:
>>>>
>>>>> Jesus,
>>>>>
>>>>> Probably the most informative information is in this blog.
>>>>>
>>>>> https://blogs.oracle.com/nashorn/entry/nashorn_multi_threading_and_mt
>>>>>
>>>>> This example uses Executors but threads would work as well.
>>>>>
>>>>> I did a talk that looked at different methods to max out multithreading
>>>>> performance. A new engine per thread is the worst case. A new context per
>>>>> thread does much better. A new global per thread is the best while
>>>>> remaining thread safe.
>>>>>
>>>>> Cheers,
>>>>>
>>>>> — Jim
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>> On Dec 6, 2016, at 8:45 AM, Jesus Luzon <jluzon at riotgames.com> wrote:
>>>>>
>>>>> Hey folks,
>>>>>
>>>>> I've tried many different ways of using Nashorn multithreaded based on
>>>>> what
>>>>> I've found on the internet and I still can't get a single one to scale.
>>>>> Even the most naive method of making many script engines with my script
>>>>> tends to bottleneck itself when I have more than 10 threads invoking
>>>>> functions.
>>>>>
>>>>> I'm using the following code to compile my script and
>>>>> invocable.invokeFunction("transform", input) to execute:
>>>>>
>>>>> static Invocable generateInvocable(String script) throws
>>>>> ScriptException {
>>>>> ScriptEngineManager manager = new ScriptEngineManager();
>>>>> ScriptEngine engine =
>>>>> manager.getEngineByName(JAVASCRIPT_ENGINE_NAME);
>>>>> Compilable compilable = (Compilable) engine;
>>>>> final CompiledScript compiled = compilable.compile(script);
>>>>> compiled.eval();
>>>>> return (Invocable) engine;
>>>>> }
>>>>>
>>>>>
>>>>>
>>>>> The script I'm compiling is:
>>>>>
>>>>> String script = "function transform(input) {" +
>>>>> "var result = JSON.parse(input);" +
>>>>> "response = {};\n" +
>>>>> "for (var i = 0; i < result.length; i++) {\n" +
>>>>> " var summoner = {};\n" +
>>>>> " summoner.id = result[i].id;\n" +
>>>>> " summoner.name = result[i].name;\n" +
>>>>> " summoner.profileIconId =
>>>>> result[i].profileIconId;\n" +
>>>>> " summoner.revisionDate = result[i].revisionDate;\n" +
>>>>> " summoner.summonerLevel = result[i].level;\n" +
>>>>> " response[summoner.id] = summoner;\n" +
>>>>> "}\n" +
>>>>> "result = response;" +
>>>>> "return JSON.stringify(result);" +
>>>>> "};";
>>>>>
>>>>>
>>>>>
>>>>> I've also tried other more scaleable ways to work with scripts
>>>>> concurrently, but given that this is the most naive method where
>>>>> everything
>>>>> is brand new and I still get slowness calling them concurrently I fear
>>>>> that
>>>>> maybe I'm overlooking something extremely basic on my code.
>>>>>
>>>>> Thanks.
>>>>> -Jesus Luzon
>>>>>
>>>>>
>>>>
>>>
>>>
>>
>>
More information about the nashorn-dev
mailing list