Skip to content

feat: improve transformation pipeline performance using Java 17 idioms#665

Merged
fborriello merged 12 commits intomasterfrom
feature/performance-improvements
Apr 27, 2026
Merged

feat: improve transformation pipeline performance using Java 17 idioms#665
fborriello merged 12 commits intomasterfrom
feature/performance-improvements

Conversation

@fborriello
Copy link
Copy Markdown
Member

@fborriello fborriello commented Apr 27, 2026

Description

  • ConversionProcessorFactory: replace 12-branch if-else chain with switch expression on class name; cache processor singletons as static constants to eliminate per-call object allocation
  • ConversionAnalyzer: replace 12-branch if-else chain in getTypeConversionFunction with switch expression on source type name, removing redundant static imports
  • ConverterImpl: use identity (==) check first before name comparison to short-circuit same-type pass-through without string allocation
  • ReflectionUtils: replace AtomicReference allocation in getRealTarget with instanceof pattern matching; eliminate redundant isAnnotationPresent call before getAnnotation; use instanceof pattern in getGenericClassType; use early-return in handleReflectionException
  • ClassUtils: replace stream().anyMatch() in isSpecialType/isCustomSpecialType with plain for-loops to avoid lambda allocation on type checks; replace LinkedList with ArrayList in getMethods for better cache locality; replace collect(toList()) with toList() (Java 16) where result is consumed read-only; replace forEach+collect with forEach(res::add) in getPrivateFinalFields, getPrivateFields, getMethods
  • AbstractTransformer: eliminate stream+lambda in withFieldMapping and withFieldTransformer inner loops, replacing with plain for-each
  • AbstractBeanTransformer: replace asList+addAll with direct for-each add in skipTransformationForField to avoid intermediate List allocation
  • MapPopulator: replace keySet().stream().collect(toMap(...)) with entrySet() imperative loop and pre-sized HashMap; apply instanceof pattern matching to eliminate getClass().equals() calls in isPrimitive and getElemValue

Checklist:

  • The branch follows the best practices naming convention:
    • Use grouping tokens (words) at the beginning of your branch names.
      • feature: Feature I'm adding or expanding
      • bug: Bugfix or experiment
      • wip: Work in progress; stuff I know won't be finished soon
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented on my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • Any dependent changes have been merged and published in downstream modules
  • My changes have no bad impacts on performances
  • My changes have been tested through Unit Tests and the overall coverage has not decreased. Check the coverall report for your branch: here
  • Any implemented change has been added in the CHANGELOG.md file
  • Any implemented change has been added in the CHANGELOG-JDK11.md file
  • My changes have been applied on a separate branch too with compatibility with Java 11. Follow this instructions for help.
  • The maven version has been increased.

- ConversionProcessorFactory: replace 12-branch if-else chain with switch
  expression on class name; cache processor singletons as static constants
  to eliminate per-call object allocation
- ConversionAnalyzer: replace 12-branch if-else chain in getTypeConversionFunction
  with switch expression on source type name, removing redundant static imports
- ConverterImpl: use identity (==) check first before name comparison to short-circuit
  same-type pass-through without string allocation
- ReflectionUtils: replace AtomicReference allocation in getRealTarget with
  instanceof pattern matching; eliminate redundant isAnnotationPresent call before
  getAnnotation; use instanceof pattern in getGenericClassType; use early-return
  in handleReflectionException
- ClassUtils: replace stream().anyMatch() in isSpecialType/isCustomSpecialType with
  plain for-loops to avoid lambda allocation on type checks; replace LinkedList with
  ArrayList in getMethods for better cache locality; replace collect(toList()) with
  toList() (Java 16) where result is consumed read-only; replace forEach+collect with
  forEach(res::add) in getPrivateFinalFields, getPrivateFields, getMethods
- AbstractTransformer: eliminate stream+lambda in withFieldMapping and
  withFieldTransformer inner loops, replacing with plain for-each
- AbstractBeanTransformer: replace asList+addAll with direct for-each add in
  skipTransformationForField to avoid intermediate List allocation
- MapPopulator: replace keySet().stream().collect(toMap(...)) with entrySet()
  imperative loop and pre-sized HashMap; apply instanceof pattern matching to
  eliminate getClass().equals() calls in isPrimitive and getElemValue

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@fborriello fborriello requested a review from a team as a code owner April 27, 2026 09:23
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 27, 2026

Test Coverage Report

Coverage Baseline Build Diff
Lines 100.00% 1236/1236 100.00% 1228/1228 +0.00% 💙
Branches 100.00% 492/492 100.00% 476/476 +0.00% 💙
Instructions 100.00% 6125/6125 100.00% 6084/6084 +0.00% 💙
Complexity 100.00% 786/786 100.00% 783/783 +0.00% 💙
Methods 100.00% 540/540 100.00% 534/534 +0.00% 💙
Classes 100.00% 50/50 100.00% 50/50 +0.00% 💙
Normalised Score 100.00% 100.00% +0.00% 💙

fborriello and others added 11 commits April 27, 2026 11:27
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Adds bull-benchmark module with 7 microbenchmarks covering:
- mutable/immutable simple bean transformation (5 fields, setter vs constructor path)
- mutable/immutable complex bean transformation (nested objects, lists, maps)
- type conversion hot paths: int->long, String->int, Boolean->BigDecimal

Run with: java -jar bull-benchmark/target/benchmarks.jar

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Adds docs/site/markdown/transformer/bean/benchmarks.md covering:
- what each of the 7 JMH benchmarks measures
- how to build the fat jar
- how to run (full, quick, single, with options)
- how to compare two branches with JSON output
- how to interpret Score/Error/Cnt columns
- how to add a new benchmark

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Adds benchmark.sh at the project root that builds the fat jar and runs
JMH in a single command; any arguments are forwarded to JMH.
Updates benchmarks.md to reference the script and simplify the Running
and Comparing sections.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…k fat jar

- Generate JMH harness sources into target/generated-sources/jmh (separate
  from classes dir) then compile them in prepare-package phase so *_jmhTest
  classes are present in the fat jar before shading
- Declare slf4j-api at compile scope to override the parent dependencyManagement
  lock to test scope, ensuring LoggerFactory is included in the shaded jar

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
After JMH finishes, the script now prints a concise formatted table of
benchmark names, scores, error intervals, and units — making results
readable at a glance without scrolling through the full JMH output.
Handles both single-iteration (no error column) and multi-iteration
(± error) JMH output formats.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Redirect Maven build stdout/stderr to a temp log (only shown on failure)
and filter NOTE:/WARNING:/Processing/Writing lines from the JMH forked-JVM
output, which originate from JDK_JAVA_OPTIONS and sun.misc.Unsafe
deprecation warnings outside our control.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…lumn

- Convert scores from µs/op to ms/op for readability
- Split summary into Bean transformations and Type conversions sections
- Show ± confidence interval in Error column when available

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- getNotFinalFields: wrap toList() in new ArrayList to keep the cached
  list mutable, preventing UnsupportedOperationException if callers mutate it
- getRealTarget: document that empty Optional unwraps to null intentionally

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@fborriello fborriello merged commit d3801e9 into master Apr 27, 2026
10 checks passed
@fborriello fborriello deleted the feature/performance-improvements branch April 27, 2026 10:57
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.

1 participant