Skip to content
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

ISSUE-292 Prevent SPI calls at runtime #293

Merged
merged 1 commit into from
Jan 6, 2023

Conversation

chibenwa
Copy link
Contributor

@chibenwa chibenwa commented Dec 15, 2022

Closes #292

@wbeckler
Copy link

What's the issue this addresses?

@wbeckler
Copy link

Ah I see it here: #292

@reta
Copy link
Collaborator

reta commented Dec 16, 2022

@chibenwa please sign with DCO (commit with--signoff)

@dblock
Copy link
Member

dblock commented Dec 16, 2022

Can we please add a unit test for this, too?

@chibenwa
Copy link
Contributor Author

Can you describe which test you want?

I expect existing test suite to cover functionnal behaviour. And testing "preventing spi calls" might require additionnal levels of indirection which i would consider harmful.

Maybe a jmh bench?

@wbeckler
Copy link

wbeckler commented Dec 30, 2022 via email

@chibenwa
Copy link
Contributor Author

Next week i'll propose a JMH bench for this changeset.

Regarding "detection of low hanging fruits" I use asyncprofiler on my workloads (memory & cpu). Can't recall other massive painpoints but I will get a second look, and share flame graphs here too.

Happy new year,

Regards,

Benoit

@chibenwa
Copy link
Contributor Author

chibenwa commented Jan 3, 2023

opensearch_2.4.0.zip

Here is the flame graph I diagnosed this issue from. Taken with https://github.com/jvm-profiling-tools/async-profiler :

./profiler -d 120 -e itimer -f opensearch_2.4.0.html 1

(From within my applicative container)

Then searching opensearch (top left) gives you:

  • The driver thread (on the right) with the httpclient event loop
  • Applicatives calls to the driver from applicative threads (small stacktraces in the center)

Observations for the driver event loop:

  • Call to the SPI are very easy to notice CF issue. Not only it eats CPU/heap allocations but also does it blocks the HTTP event loop, which is catastrophic (my app is not OpenSearch heavy so that is not a big issue to me).
  • 0.5% of my application CPU (and 5% of the eventLoop CPU - similar footprint on heap allocation) is eaten by HTTP-client basic auth caching... :-( Likely a low hanging fruit, shall I create an issue for this one?
  • Unsurprisingly JSON parsing takes ~11% of the drivers CPU and 24% of heap allocation.
  • Event loop busyness (push stuff to the kernel network stack, SSL) takes around 60% of the event loop CPU, 25% of heap allocations, which again feels normal to me.
  • 2.3% of the event loop CPU is our binding to Reactor reactive library, which, again is normal: converting futures and enqueuing tasks takes time.

@chibenwa
Copy link
Contributor Author

chibenwa commented Jan 3, 2023

JMH output backing this change

Before

Benchmark                Mode  Cnt   Score   Error  Units
JMHFieldBench.jsonBench  avgt    5  71.790 ± 2.125  us/op

After

Benchmark                Mode  Cnt  Score   Error  Units
JMHFieldBench.jsonBench  avgt    5  1.418 ± 0.024  us/op

Conclusion

x50 fastening of the JSOn parsing by getting rid of the SPI lookups, not event taking into account blocking operations... Huge win!

Code of the benchmark

package org.apache.james.backends.opensearch;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

import org.apache.james.backends.opensearch.json.jackson.JacksonJsonpParser;
import org.junit.Test;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.TimeValue;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JMHFieldBench {

    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    @Test
    public void launchBenchmark() throws Exception {
        Options opt = new OptionsBuilder()
            .include(this.getClass().getName() + ".*")
            .mode (Mode.AverageTime)
            .timeUnit(TimeUnit.MICROSECONDS)
            .warmupTime(TimeValue.seconds(5))
            .warmupIterations(3)
            .measurementTime(TimeValue.seconds(2))
            .measurementIterations(5)
            .threads(1)
            .forks(1)
            .shouldFailOnError(true)
            .shouldDoGC(true)
            .build();

        new Runner(opt).run();
    }

    @Benchmark
    public void jsonBench(Blackhole bh) throws IOException {
        final JsonParser parser = OBJECT_MAPPER.createParser("[\"a\",\"b\"]");
        try {
            bh.consume(new JacksonJsonpParser(parser).getArrayStream());
        } catch (Exception e) {
            // do nothing
        } finally {
            parser.close();
        }
    }
}

The bench was applied before/after the backport of this PR on top of Apache James CF apache/james-project@d5af3a5

(Note this backport is uggly and I would like to drop it as fast as possible!)

@reta
Copy link
Collaborator

reta commented Jan 3, 2023

It is known that ObjectMapper is expensive to create, making it static certainly helps

@chibenwa
Copy link
Contributor Author

chibenwa commented Jan 4, 2023

It is known that ObjectMapper is expensive to create, making it static certainly helps

I do not see how this affects this benchmark

@reta
Copy link
Collaborator

reta commented Jan 4, 2023

It is known that ObjectMapper is expensive to create, making it static certainly helps

I do not see how this affects this benchmark

Apologies, your benchmark checks the impact of changing the JsonProvider.provider() initialization (instance -> static), for some reason I thought that JsonProvider.provider() was instantiating ObjectMapper instances (which is not correct).

@wbeckler
Copy link

wbeckler commented Jan 4, 2023

Would you be up for adding a change to the changelog?

Signed-off-by: Benoit Tellier <btellier@linagora.com>
@dblock dblock merged commit a8df7e7 into opensearch-project:main Jan 6, 2023
@reta reta added the backport 2.x Backport to 2.x branch label Jan 6, 2023
@opensearch-trigger-bot
Copy link
Contributor

The backport to 2.x failed:

The process '/usr/bin/git' failed with exit code 1

To backport manually, run these commands in your terminal:

# Fetch latest updates from GitHub
git fetch
# Create a new working tree
git worktree add .worktrees/backport-2.x 2.x
# Navigate to the new working tree
cd .worktrees/backport-2.x
# Create a new branch
git switch --create backport/backport-293-to-2.x
# Cherry-pick the merged commit of this pull request and resolve the conflicts
git cherry-pick -x --mainline 1 a8df7e7c26ccc644811539c4fea57d97f1031aaa
# Push it to GitHub
git push --set-upstream origin backport/backport-293-to-2.x
# Go back to the original working tree
cd ../..
# Delete the working tree
git worktree remove .worktrees/backport-2.x

Then, create a pull request where the base branch is 2.x and the compare/head branch is backport/backport-293-to-2.x.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport 2.x Backport to 2.x branch
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BUG] SPI lookups on the HTTP client eventloop using JacksonJsonpMapper
4 participants