This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/pdfbox-jbig2.git
commit cb40b3cd0439fd8f7f89f36109fc9e72bc905010 Author: Maruan Sahyoun <[email protected]> AuthorDate: Wed May 13 13:12:35 2026 +0200 PDFBOX-6151: Add ArithmeticDecoderBenchmark with JMH/JFR profiling infrastructure --- pom.xml | 58 ++++++++- .../arithmetic/ArithmeticDecoderBenchmark.java | 132 +++++++++++++++++++++ 2 files changed, 189 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bc2e316..4c4aa07 100644 --- a/pom.xml +++ b/pom.xml @@ -432,6 +432,62 @@ </plugins> </build> </profile> + +<profile> + <id>benchmark-jfr</id> + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>1.8</source> + <target>1.8</target> + <debug>false</debug> + <optimize>true</optimize> + <annotationProcessorPaths> + <path> + <groupId>org.openjdk.jmh</groupId> + <artifactId>jmh-generator-annprocess</artifactId> + <version>1.37</version> + </path> + </annotationProcessorPaths> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>3.1.0</version> + <executions> + <execution> + <id>run-benchmarks-jfr</id> + <phase>test</phase> + <goals> + <goal>exec</goal> <!-- forked JVM — JFR sees only JMH --> + </goals> + <configuration> + <executable>java</executable> + <classpathScope>test</classpathScope> + <arguments> + <argument>-classpath</argument> + <classpath/> <!-- expands to full test classpath --> + <argument>-XX:+FlightRecorder</argument> + <argument>-XX:StartFlightRecording=filename=${project.build.directory}/arithmetic-decoder.jfr,settings=profile,dumponexit=true</argument> + <argument>org.openjdk.jmh.Main</argument> + <argument>.*ArithmeticDecoder.*</argument> + <argument>-rf</argument> + <argument>json</argument> + <argument>-rff</argument> + <argument>${project.build.directory}/benchmark-results.json</argument> + </arguments> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</profile> + + <profile> <id>benchmark</id> <build> @@ -467,7 +523,7 @@ <mainClass>org.openjdk.jmh.Main</mainClass> <classpathScope>test</classpathScope> <arguments> - <argument>.*Benchmark.*</argument> + <argument>.*DecoderBenchmark.*</argument> <argument>-rf</argument> <argument>json</argument> <argument>-rff</argument> diff --git a/src/test/java/org/apache/pdfbox/jbig2/decoder/arithmetic/ArithmeticDecoderBenchmark.java b/src/test/java/org/apache/pdfbox/jbig2/decoder/arithmetic/ArithmeticDecoderBenchmark.java new file mode 100644 index 0000000..816c631 --- /dev/null +++ b/src/test/java/org/apache/pdfbox/jbig2/decoder/arithmetic/ArithmeticDecoderBenchmark.java @@ -0,0 +1,132 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.jbig2.decoder.arithmetic; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.TimeUnit; + +/** + * Baseline benchmark for ArithmeticDecoder. + * + * Run with: mvn clean test -Pbenchmark + * Results: target/benchmark-results.json + * + * Fork is intentionally 0 — the benchmark profile uses exec-maven-plugin + * which already provides an isolated JVM. Warmup is increased to compensate. + */ +@State(Scope.Thread) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS) // 10s iterations +@Fork(0) +public class ArithmeticDecoderBenchmark { + + private byte[] encodedBytes; + private ArithmeticDecoder decoder; + + private int symbolsDecoded; + private volatile int expectedSymbolCount = -1; + + // ----------------------------------------------------------------------- + // Setup / TearDown + // ----------------------------------------------------------------------- + + @Setup(Level.Trial) + public void loadBytes() throws IOException { + InputStream is = getClass() + .getResourceAsStream("/images/arith/encoded testsequence"); + if (is == null) { + throw new IllegalStateException( + "Test resource not found. Run 'mvn generate-test-resources' first."); + } + encodedBytes = toByteArray(is); + is.close(); + expectedSymbolCount = -1; + } + + @Setup(Level.Invocation) + public void setup() throws IOException { + // MemoryCacheImageInputStream — pure in-memory, eliminates the + // FileCacheImageInputStream temp-file backed native seek() calls + // that were consuming 32.7% of CPU time per JFR profiling. + ImageInputStream iis = new MemoryCacheImageInputStream( + new ByteArrayInputStream(encodedBytes)); + decoder = new ArithmeticDecoder(iis); + symbolsDecoded = 0; + } + + @TearDown(Level.Invocation) + public void verifySymbolCount() { + if (expectedSymbolCount == -1) { + expectedSymbolCount = symbolsDecoded; + System.out.printf( + "[ArithmeticDecoderBenchmark] baseline symbol count: %d%n", expectedSymbolCount); + } else if (symbolsDecoded != expectedSymbolCount) { + throw new IllegalStateException(String.format( + "Symbol count mismatch: expected %d but decoded %d. " + + "Check byteIn() for early termination or over-read bugs.", + expectedSymbolCount, symbolsDecoded)); + } + } + + // ----------------------------------------------------------------------- + // Benchmark + // ----------------------------------------------------------------------- + + @Benchmark + public void decodeFullStream(Blackhole bh) throws IOException { + CX cx = new CX(1, 0); + for (int i = 0; i < 257; i++) { // fixed count matching the test + bh.consume(decoder.decode(cx)); + symbolsDecoded++; + } + } + // ----------------------------------------------------------------------- + // Utility + // ----------------------------------------------------------------------- + + /** + * Heuristic to distinguish normal end-of-stream termination from a + * genuine decode error. ImageInputStream implementations typically + * produce a null or empty message on EOF. + */ + private static boolean isEndOfStream(IOException e) { + String msg = e.getMessage(); + return msg == null || msg.isEmpty(); + } + + /** Java-8-compatible equivalent of InputStream.readAllBytes(). */ + private static byte[] toByteArray(InputStream is) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] chunk = new byte[4096]; + int bytesRead; + while ((bytesRead = is.read(chunk)) != -1) { + buffer.write(chunk, 0, bytesRead); + } + return buffer.toByteArray(); + } +} \ No newline at end of file
