diff --git a/performance-test/pom.xml b/performance-test/pom.xml new file mode 100644 index 00000000..159b6469 --- /dev/null +++ b/performance-test/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + io.github.java-helpers + simple-builders-performance-test + 0.6.0-SNAPSHOT + jar + + Simple Builders - Performance Test + Performance test module with 1000 DTO classes for measuring builder generation performance. Not part of the default build. + https://github.com/java-helpers/simple-builders + 2026 + + + 17 + UTF-8 + + 3.20.0 + + ${java.version} + ${java.version} + ${java.version} + + + false + false + + 3.15.0 + 3.1.4 + + + + + io.github.java-helpers + simple-builders-core + ${project.version} + + + org.apache.commons + commons-lang3 + ${commons-lang.version} + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + ${plugin.maven.deploy.version} + + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${plugin.maven.compiler.version} + + ${java.version} + ${java.version} + ${java.version} + ${project.basedir}/generated-performance-builder + + + + default-compile + + + + io.github.java-helpers + simple-builders-processor + ${project.version} + + + + -Averbose=${simplebuilder.verbose} + -Asimplebuilder.performanceTracking=${simplebuilder.performanceTracking} + + + + + default-testCompile + + none + + + + + + + diff --git a/pom.xml b/pom.xml index 24235cd6..ec1aeaee 100644 --- a/pom.xml +++ b/pom.xml @@ -172,5 +172,11 @@ + + performance-test + + performance-test + + \ No newline at end of file diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java index d89d43e1..8ebb3266 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java @@ -26,6 +26,10 @@ import static org.javahelpers.simple.builders.processor.model.core.BuilderToGenerationTypeMapper.toRenderingDto; import static org.javahelpers.simple.builders.processor.processing.BuilderDefinitionCreator.extractFromElement; +import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_BUILDER_DEFINITION_EXTRACTION; +import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_CODE_GENERATION; +import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION; +import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_DTO_MAPPING; import com.google.auto.service.AutoService; import java.util.ArrayList; @@ -56,7 +60,8 @@ import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsEnum; import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; -import org.javahelpers.simple.builders.processor.processing.ProcessingLogger; +import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; /** * BuilderProcessor is an annotation processor for execution in generate-sources phase. The @@ -83,7 +88,8 @@ public synchronized void init(ProcessingEnvironment processingEnv) { logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig); this.context = new ProcessingContext(logger, globalConfig, processingEnv); - this.codeGenerator = new RoasterCodeGenerator(processingEnv, logger); + this.codeGenerator = + new RoasterCodeGenerator(processingEnv, logger, context.getPerformanceTracker()); this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger); // Initialize GeneratorRegistry once during processor initialization @@ -112,6 +118,10 @@ public boolean process(Set annotations, RoundEnvironment // Generate Jackson Module if processing is over and feature is enabled if (roundEnv.processingOver()) { + // Generate performance report at the end of processing + PerformanceTracker tracker = context.getPerformanceTracker(); + tracker.generateReport(context.getLogger()); + List moduleClassDefs = jacksonModuleGenerator.getModuleDefinitions(); for (GenerationTargetClassDto moduleClassDef : moduleClassDefs) { @@ -175,13 +185,17 @@ public boolean process(Set annotations, RoundEnvironment .sorted(Comparator.comparing(element -> element.getSimpleName().toString())) .toList(); + PerformanceTracker tracker = context.getPerformanceTracker(); int successfulGenerations = 0; for (Element annotatedElement : sortedElements) { context.debugStartOperation("Processing element: " + annotatedElement.getSimpleName()); + String className = annotatedElement.getSimpleName().toString(); + tracker.startClass(className); try { - // Resolve configuration per-element to handle all layers - // (defaults, global, template, inline) + // Track Configuration Resolution (actual work happens here) + tracker.startPhase(PHASE_CONFIGURATION_RESOLUTION, className); BuilderConfiguration config = reader.resolveConfiguration(annotatedElement); + tracker.endPhase(PHASE_CONFIGURATION_RESOLUTION); context.debug("Configuration resolved: %s", config); process(annotatedElement, config); successfulGenerations++; @@ -225,14 +239,46 @@ public SourceVersion getSupportedSourceVersion() { private void process(Element annotatedElement, BuilderConfiguration config) throws BuilderException { context.initConfigurationForProcessingTarget(config); + PerformanceTracker tracker = context.getPerformanceTracker(); + String className = annotatedElement.getSimpleName().toString(); + + // Track Builder Definition Extraction + tracker.startPhase(PHASE_BUILDER_DEFINITION_EXTRACTION, className); BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, context); + tracker.endPhase(PHASE_BUILDER_DEFINITION_EXTRACTION); + + // Compute class-level metrics now that the definition is available + int fieldCount = builderDef.getAllFieldsForBuilder().size(); + int collectionCount = + (int) + builderDef.getAllFieldsForBuilder().stream() + .filter( + f -> { + String typeName = f.getFieldType().getFullQualifiedName(); + return typeName.startsWith("java.util.List") + || typeName.startsWith("java.util.Set") + || typeName.startsWith("java.util.Map") + || typeName.startsWith("java.util.Collection"); + }) + .count(); + + // Track DTO Mapping + tracker.startPhase(PHASE_DTO_MAPPING, className); GenerationTargetClassDto renderingDto = toRenderingDto(builderDef); + tracker.endPhase(PHASE_DTO_MAPPING); + + // Track Code Generation (parent phase; sub-phases tracked inside RoasterCodeGenerator) + tracker.startPhase(PHASE_CODE_GENERATION, className); codeGenerator.generateClass(renderingDto); + tracker.endPhase(PHASE_CODE_GENERATION); // Collect info for Jackson Module if enabled jacksonModuleGenerator.addEntry(builderDef, annotatedElement); context.debug("Jackson module entry added"); + // End class-level timing (started in caller before config resolution) + tracker.endClass(fieldCount, collectionCount); + // Add summary of what was generated context.debugEndOperation( "Generated builder with %d fields and %d methods for %s", diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 5e3f5df1..a8f8b8f4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -26,6 +26,7 @@ import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapType; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; +import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.*; import java.io.IOException; import java.io.InputStream; @@ -58,7 +59,8 @@ import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; -import org.javahelpers.simple.builders.processor.processing.ProcessingLogger; +import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; import org.javahelpers.simple.builders.processor.util.ImportCollector; import org.jboss.forge.roaster.Roaster; import org.jboss.forge.roaster.model.source.AnnotationSource; @@ -81,6 +83,9 @@ public class RoasterCodeGenerator { /** Logger for debug output during code generation. */ private final ProcessingLogger logger; + /** Performance tracker for sub-phase timing (Source Construction, File Writing). */ + private final PerformanceTracker performanceTracker; + private final Properties formatterProperties; /** @@ -89,9 +94,11 @@ public class RoasterCodeGenerator { * @param processingEnv Processing environment for accessing filer and element utilities * @param logger Logger for debug output */ - public RoasterCodeGenerator(ProcessingEnvironment processingEnv, ProcessingLogger logger) { + public RoasterCodeGenerator( + ProcessingEnvironment processingEnv, ProcessingLogger logger, PerformanceTracker tracker) { this.processingEnv = processingEnv; this.logger = logger; + this.performanceTracker = tracker; this.formatterProperties = loadFormatterProperties(); } @@ -107,20 +114,28 @@ public void generateClass(GenerationTargetClassDto classDef) throws BuilderExcep String sourceCode; try { - sourceCode = createClassSource(classDef); + String className = classDef.getTypeName().getClassName(); + performanceTracker.startPhase(PHASE_SOURCE_CONSTRUCTION, className); + JavaClassSource source = buildClassSource(classDef); + sourceCode = renderClassSource(source, className); + performanceTracker.endPhase(PHASE_SOURCE_CONSTRUCTION); } catch (RuntimeException ex) { // Rendering failures (e.g. RoasterMapperException) are RuntimeExceptions. Convert them into // a BuilderException so callers can isolate the failure to this single class and keep // generating the remaining builders instead of aborting the whole processing round. throw new BuilderException(null, ex); } + performanceTracker.startPhase(PHASE_FILE_WRITING, classDef.getTypeName().getClassName()); writeClassToFile(sourceCode, classDef); + performanceTracker.endPhase(PHASE_FILE_WRITING); logger.debugEndOperation( "Successfully generated class: %s", classDef.getTypeName().getClassName()); } - private String createClassSource(GenerationTargetClassDto classDef) { + private JavaClassSource buildClassSource(GenerationTargetClassDto classDef) { + String className = classDef.getTypeName().getClassName(); + performanceTracker.startPhase(PHASE_ELEMENT_BUILDING, className); JavaClassSource source = createJavaClassSource(classDef); addClassMetadata(source, classDef); appendFields(source, classDef); @@ -128,19 +143,22 @@ private String createClassSource(GenerationTargetClassDto classDef) { appendMethods(source, classDef); appendNestedTypes(source, classDef); applyClassAnnotations(source, classDef); - return renderClassSource(source); + performanceTracker.endPhase(PHASE_ELEMENT_BUILDING); + return source; } private void applyClassAnnotations(JavaClassSource source, GenerationTargetClassDto classDef) { - if (CollectionUtils.isEmpty(classDef.getClassAnnotations())) { - return; + performanceTracker.startPhase(PHASE_CLASS_ANNOTATIONS, classDef.getTypeName().getClassName()); + if (CollectionUtils.isNotEmpty(classDef.getClassAnnotations())) { + // Adding class annotations + applyAnnotations(source, classDef.getClassAnnotations()); + logger.debug("Class-level annotations added"); } - // Adding class annotations - applyAnnotations(source, classDef.getClassAnnotations()); - logger.debug("Class-level annotations added"); + performanceTracker.endPhase(PHASE_CLASS_ANNOTATIONS); } private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) { + performanceTracker.startPhase(PHASE_CLASS_CREATION, classDef.getTypeName().getClassName()); if (CollectionUtils.isNotEmpty(classDef.getGenerics())) { logger.debug("Class has %d generic type parameter(s)", classDef.getGenerics().size()); } @@ -166,10 +184,12 @@ private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) } logger.debug("JavaClassSource created"); + performanceTracker.endPhase(PHASE_CLASS_CREATION); return source; } private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto classDef) { + performanceTracker.startPhase(PHASE_CLASS_METADATA, classDef.getTypeName().getClassName()); applyJavadoc(source, classDef.getClassJavadoc()); applyVisibility(source, classDef.getClassAccessModifier()); applySuperType(source, classDef.getSuperType()); @@ -180,20 +200,26 @@ private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto c } logger.debug("Class metadata added"); + performanceTracker.endPhase(PHASE_CLASS_METADATA); } - private String renderClassSource(JavaClassSource source) { + private String renderClassSource(JavaClassSource source, String className) { + performanceTracker.startPhase(PHASE_STRING_GENERATION, className); String rendered = source.toUnformattedString(); + performanceTracker.endPhase(PHASE_STRING_GENERATION); + performanceTracker.startPhase(PHASE_FORMATTING, className); String formatted = formatSource(rendered); // Roaster renders some java.lang annotations (e.g. @SuppressWarnings, @Deprecated with // members) with their FQN (@java.lang.SuppressWarnings) even though java.lang types don't // need qualification. Fix this by replacing @java.lang.Xxx with @Xxx for known annotations. formatted = formatted.replace("@java.lang.SuppressWarnings", "@SuppressWarnings"); formatted = formatted.replace("@java.lang.Deprecated", "@Deprecated"); + performanceTracker.endPhase(PHASE_FORMATTING); return formatted; } private void appendFields(JavaClassSource source, GenerationTargetClassDto classDef) { + performanceTracker.startPhase(PHASE_FIELDS, classDef.getTypeName().getClassName()); logger.debugStartOperation("Generating %d fields", classDef.getClassFields().size()); for (ClassFieldDto fieldDto : classDef.getClassFields()) { @@ -201,6 +227,7 @@ private void appendFields(JavaClassSource source, GenerationTargetClassDto class } logger.debugEndOperation("Fields added: %d fields", source.getFields().size()); + performanceTracker.endPhase(PHASE_FIELDS); } private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { @@ -213,6 +240,7 @@ private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { } private void appendConstructors(JavaClassSource source, GenerationTargetClassDto classDef) { + performanceTracker.startPhase(PHASE_CONSTRUCTORS, classDef.getTypeName().getClassName()); logger.debugStartOperation("Generating %d constructors", classDef.getConstructors().size()); for (ConstructorDto constructor : classDef.getConstructors()) { @@ -220,6 +248,7 @@ private void appendConstructors(JavaClassSource source, GenerationTargetClassDto } logger.debugEndOperation("Constructors added: %d", classDef.getConstructors().size()); + performanceTracker.endPhase(PHASE_CONSTRUCTORS); } private void appendConstructor(JavaClassSource source, ConstructorDto constructor) { @@ -235,6 +264,7 @@ private void appendConstructor(JavaClassSource source, ConstructorDto constructo } private void appendMethods(JavaClassSource source, GenerationTargetClassDto classDef) { + performanceTracker.startPhase(PHASE_METHODS, classDef.getTypeName().getClassName()); logger.debugStartOperation("Generating %d method candidates", classDef.getMethods().size()); // Resolve method conflicts by signature and priority @@ -246,6 +276,7 @@ private void appendMethods(JavaClassSource source, GenerationTargetClassDto clas } logger.debugEndOperation("Methods added: %d", resolvedMethods.size()); + performanceTracker.endPhase(PHASE_METHODS); } /** @@ -297,15 +328,16 @@ private void appendMethod( } private void appendNestedTypes(JavaClassSource source, GenerationTargetClassDto classDef) { - if (CollectionUtils.isEmpty(classDef.getNestedTypes())) { - return; - } - logger.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size()); - for (NestedTypeDto nestedType : classDef.getNestedTypes()) { - appendNestedType(source, nestedType); - logger.debug("Generated nested type: %s", nestedType.getTypeName()); + performanceTracker.startPhase(PHASE_NESTED_TYPES, classDef.getTypeName().getClassName()); + if (CollectionUtils.isNotEmpty(classDef.getNestedTypes())) { + logger.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size()); + for (NestedTypeDto nestedType : classDef.getNestedTypes()) { + appendNestedType(source, nestedType); + logger.debug("Generated nested type: %s", nestedType.getTypeName()); + } + logger.debugEndOperation("Nested types added"); } - logger.debugEndOperation("Nested types added"); + performanceTracker.endPhase(PHASE_NESTED_TYPES); } private void appendNestedType(JavaClassSource source, NestedTypeDto nestedType) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java index 6c7e96a0..7c4faf17 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java @@ -41,7 +41,7 @@ import org.javahelpers.simple.builders.processor.model.method.MethodCodeDto; import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; -import org.javahelpers.simple.builders.processor.processing.ProcessingLogger; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; /** * Generates Jackson SimpleModules to register all generated builders. diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java index ef0ebce8..9638c47e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java @@ -38,6 +38,7 @@ import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; +import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker; /** * Unified registry that manages all generators (both method generators and builder enhancers). @@ -60,6 +61,7 @@ public class GeneratorRegistry { private final List builderEnhancers; private final ProcessingContext context; private final ComponentFilter componentFilter; + private final PerformanceTracker performanceTracker; /** * Creates a new registry and initializes it with built-in and custom generators. @@ -72,6 +74,7 @@ public GeneratorRegistry(ProcessingContext context, ProcessingEnvironment proces this.methodGenerators = new ArrayList<>(); this.builderEnhancers = new ArrayList<>(); this.componentFilter = new ComponentFilter(processingEnv); + this.performanceTracker = context.getPerformanceTracker(); loadAllGenerators(); sortGeneratorsByPriority(); @@ -102,8 +105,11 @@ public List generateAllMethods( "Applying: %s (priority: %d)", generator.getClass().getSimpleName(), generator.getPriority()); + String genName = generator.getClass().getSimpleName(); + performanceTracker.startGenerator(genName); List generatedMethods = generator.generateMethods(field, builderType, context); + performanceTracker.endGenerator(genName); if (CollectionUtils.isNotEmpty(generatedMethods)) { allMethods.addAll(generatedMethods); @@ -136,7 +142,10 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, TypeName dtoType) { "Applying: %s (priority: %d)", enhancer.getClass().getSimpleName(), enhancer.getPriority()); + String enhName = enhancer.getClass().getSimpleName(); + performanceTracker.startEnhancer(enhName); enhancer.enhanceBuilder(builderDto, context); + performanceTracker.endEnhancer(enhName); appliedEnhancers++; } catch (Exception e) { context.error( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java index 6abb5259..49398368 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java @@ -39,6 +39,7 @@ import org.javahelpers.simple.builders.core.enums.OptionState; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; /** * Reads builder configuration from annotated elements. diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java index cf40d714..4703b5b0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java @@ -133,6 +133,10 @@ public enum CompilerArgumentsEnum { /** Option for verbose logging output. */ VERBOSE("verbose"), + // === Performance Tracking === + /** Option for performance tracking during annotation processing. */ + PERFORMANCE_TRACKING("performanceTracking"), + // === Error Handling === /** * Option for strict/fail-fast generation mode. When enabled, builder (and Jackson module) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java index 2886227f..ca2c832d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java @@ -35,6 +35,10 @@ import org.javahelpers.simple.builders.processor.generators.registry.GeneratorRegistry; import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.processing.logging.ActivePerformanceTracker; +import org.javahelpers.simple.builders.processor.processing.logging.NoOpPerformanceTracker; +import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; /** * Context object that wraps Elements, Types, and logging utilities from annotation processing, @@ -48,6 +52,7 @@ public final class ProcessingContext { private final ProcessingLogger logger; private final BuilderConfigurationReader configurationReader; private final ProcessingEnvironment processingEnv; + private final PerformanceTracker performanceTracker; private GeneratorRegistry generatorRegistry; private BuilderConfiguration configurationForProcessingTarget; @@ -68,6 +73,12 @@ public ProcessingContext( this.processingEnv = processingEnv; this.configurationReader = new BuilderConfigurationReader(globalConfiguration, logger, elementUtils); + // Initialize performance tracker based on compiler argument + CompilerArgumentsReader argReader = new CompilerArgumentsReader(processingEnv); + boolean perfTrackingEnabled = + argReader.readBooleanValue(CompilerArgumentsEnum.PERFORMANCE_TRACKING); + this.performanceTracker = + perfTrackingEnabled ? new ActivePerformanceTracker() : new NoOpPerformanceTracker(); // GeneratorRegistry will be lazily initialized on first access } @@ -113,6 +124,19 @@ public GeneratorRegistry getGeneratorRegistry() { return generatorRegistry; } + /** + * Gets the performance tracker for this processing context. + * + *

When performance tracking is disabled, returns a {@link NoOpPerformanceTracker} that has + * zero overhead. When enabled via {@code -Asimplebuilder.performanceTracking=true}, returns an + * {@link ActivePerformanceTracker} that measures execution times. + * + * @return the performance tracker instance + */ + public PerformanceTracker getPerformanceTracker() { + return performanceTracker; + } + /** * Get the TypeElement for a given qualified class name. * @@ -229,6 +253,15 @@ public java.util.List directSupertypes(TypeMirror typeMirr return typeUtils.directSupertypes(typeMirror); } + /** + * Gets the processing logger for this context. + * + * @return the processing logger + */ + public ProcessingLogger getLogger() { + return logger; + } + /** * Logs an info-level message that appears in normal Maven output. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTracker.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTracker.java new file mode 100644 index 00000000..53a4d419 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTracker.java @@ -0,0 +1,304 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.processing.logging; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Active implementation of {@link PerformanceTracker} that measures execution times using {@link + * System#nanoTime()} and aggregates results for a summary report. + * + *

This tracker maintains: + * + *

    + *
  • Per-phase total time with a hardcoded hierarchy for display: + *
    + *       Configuration Resolution
    + *       Builder Definition Extraction
    + *       DTO Mapping
    + *       Code Generation
    + *       ├─ Source Construction
    + *       │  ├─ Element Building
    + *       │  │  ├─ Class Creation
    + *       │  │  ├─ Class Metadata
    + *       │  │  ├─ Fields
    + *       │  │  ├─ Constructors
    + *       │  │  ├─ Methods
    + *       │  │  ├─ Nested Types
    + *       │  │  └─ Class Annotations
    + *       │  ├─ String Generation
    + *       │  └─ Formatting
    + *       └─ File Writing
    + *       
    + * Percentages are calculated relative to the parent phase. + *
  • Per-generator total time and call count (for MethodGenerators) + *
  • Per-enhancer total time and call count (for BuilderEnhancers) + *
  • Per-class total time with field count and collection count + *
+ */ +public final class ActivePerformanceTracker implements PerformanceTracker { + + /** Hardcoded phase hierarchy for report display. Order defines display order. */ + private static final List TOP_LEVEL_PHASES = + List.of( + PHASE_CONFIGURATION_RESOLUTION, + PHASE_BUILDER_DEFINITION_EXTRACTION, + PHASE_DTO_MAPPING, + PHASE_CODE_GENERATION); + + private static final Map> PHASE_CHILDREN = new LinkedHashMap<>(); + + static { + PHASE_CHILDREN.put( + PHASE_CODE_GENERATION, List.of(PHASE_SOURCE_CONSTRUCTION, PHASE_FILE_WRITING)); + PHASE_CHILDREN.put( + PHASE_SOURCE_CONSTRUCTION, + List.of(PHASE_ELEMENT_BUILDING, PHASE_STRING_GENERATION, PHASE_FORMATTING)); + PHASE_CHILDREN.put( + PHASE_ELEMENT_BUILDING, + List.of( + PHASE_CLASS_CREATION, + PHASE_CLASS_METADATA, + PHASE_FIELDS, + PHASE_CONSTRUCTORS, + PHASE_METHODS, + PHASE_NESTED_TYPES, + PHASE_CLASS_ANNOTATIONS)); + } + + private final Map phaseTimes = new LinkedHashMap<>(); + private final Map generatorTimes = new LinkedHashMap<>(); + private final Map generatorCalls = new LinkedHashMap<>(); + private final Map enhancerTimes = new LinkedHashMap<>(); + private final Map enhancerCalls = new LinkedHashMap<>(); + private final List classMetrics = new ArrayList<>(); + + private final ThreadLocal> phaseStartStack = ThreadLocal.withInitial(ArrayList::new); + private final ThreadLocal> generatorStartStack = + ThreadLocal.withInitial(ArrayList::new); + private final ThreadLocal> enhancerStartStack = + ThreadLocal.withInitial(ArrayList::new); + + private long totalStartTime; + private int totalClasses = 0; + + private String currentClassName; + private long classStartTime; + + /** Creates a new ActivePerformanceTracker and records the overall start time. */ + public ActivePerformanceTracker() { + this.totalStartTime = System.nanoTime(); + } + + @Override + public void startPhase(String phase, String className) { + phaseStartStack.get().add(System.nanoTime()); + } + + @Override + public void endPhase(String phase) { + List stack = phaseStartStack.get(); + if (stack.isEmpty()) { + return; + } + long start = stack.remove(stack.size() - 1); + long elapsed = System.nanoTime() - start; + phaseTimes.merge(phase, elapsed, Long::sum); + } + + @Override + public void startGenerator(String generatorName) { + generatorStartStack.get().add(System.nanoTime()); + } + + @Override + public void endGenerator(String generatorName) { + List stack = generatorStartStack.get(); + if (stack.isEmpty()) { + return; + } + long start = stack.remove(stack.size() - 1); + long elapsed = System.nanoTime() - start; + generatorTimes.merge(generatorName, elapsed, Long::sum); + generatorCalls.merge(generatorName, 1, Integer::sum); + } + + @Override + public void startEnhancer(String enhancerName) { + enhancerStartStack.get().add(System.nanoTime()); + } + + @Override + public void endEnhancer(String enhancerName) { + List stack = enhancerStartStack.get(); + if (stack.isEmpty()) { + return; + } + long start = stack.remove(stack.size() - 1); + long elapsed = System.nanoTime() - start; + enhancerTimes.merge(enhancerName, elapsed, Long::sum); + enhancerCalls.merge(enhancerName, 1, Integer::sum); + } + + @Override + public void startClass(String className) { + this.currentClassName = className; + this.classStartTime = System.nanoTime(); + } + + @Override + public void endClass(int fieldCount, int collectionCount) { + if (currentClassName == null) { + return; + } + long elapsed = System.nanoTime() - classStartTime; + classMetrics.add(new ClassMetric(currentClassName, elapsed, fieldCount, collectionCount)); + totalClasses++; + currentClassName = null; + } + + @Override + public void generateReport(ProcessingLogger logger) { + long totalTime = System.nanoTime() - totalStartTime; + double totalSeconds = totalTime / 1_000_000_000.0; + + logger.info("simple-builders: PERFORMANCE REPORT"); + logger.info("================================"); + logger.info("Total classes processed: %d", totalClasses); + logger.info(String.format(Locale.US, "Total processing time: %.1fs", totalSeconds)); + logger.info(""); + + // Phase breakdown (hierarchical, using hardcoded hierarchy) + logger.info("Phase breakdown:"); + for (int i = 0; i < TOP_LEVEL_PHASES.size(); i++) { + reportPhase( + logger, TOP_LEVEL_PHASES.get(i), totalSeconds, "", i == TOP_LEVEL_PHASES.size() - 1); + } + logger.info(""); + + // Average per class + if (totalClasses > 0) { + double avgPerClass = (totalTime / 1_000_000.0) / totalClasses; + logger.info(String.format(Locale.US, "Average per class: %.1fms", avgPerClass)); + logger.info(""); + } + + // Top 20 slowest classes + List topClasses = new ArrayList<>(classMetrics); + topClasses.sort(Comparator.comparingLong(ClassMetric::elapsedNanos).reversed()); + int classLimit = Math.min(20, topClasses.size()); + if (classLimit > 0) { + logger.info("Top %d slowest classes:", classLimit); + for (int i = 0; i < classLimit; i++) { + ClassMetric cm = topClasses.get(i); + double ms = cm.elapsedNanos() / 1_000_000.0; + logger.info( + String.format( + Locale.US, + " %d. %s - %.1fms (%d fields, %d collections)", + i + 1, + cm.className(), + ms, + cm.fieldCount(), + cm.collectionCount())); + } + logger.info(""); + } + + // Top 5 slowest MethodGenerators + List> topGenerators = new ArrayList<>(generatorTimes.entrySet()); + topGenerators.sort(Map.Entry.comparingByValue().reversed()); + int genLimit = Math.min(5, topGenerators.size()); + if (genLimit > 0) { + logger.info("Top %d slowest MethodGenerators:", genLimit); + for (int i = 0; i < genLimit; i++) { + Map.Entry entry = topGenerators.get(i); + double seconds = entry.getValue() / 1_000_000_000.0; + int calls = generatorCalls.getOrDefault(entry.getKey(), 0); + double avgMs = calls > 0 ? (entry.getValue() / 1_000_000.0) / calls : 0; + logger.info( + String.format( + Locale.US, + " %d. %s - %.1fs (%d calls, %.2fms/call)", + i + 1, + entry.getKey(), + seconds, + calls, + avgMs)); + } + logger.info(""); + } + + // Top 5 slowest BuilderEnhancers + List> topEnhancers = new ArrayList<>(enhancerTimes.entrySet()); + topEnhancers.sort(Map.Entry.comparingByValue().reversed()); + int enhLimit = Math.min(5, topEnhancers.size()); + if (enhLimit > 0) { + logger.info("Top %d slowest BuilderEnhancers:", enhLimit); + for (int i = 0; i < enhLimit; i++) { + Map.Entry entry = topEnhancers.get(i); + double seconds = entry.getValue() / 1_000_000_000.0; + int calls = enhancerCalls.getOrDefault(entry.getKey(), 0); + double avgMs = calls > 0 ? (entry.getValue() / 1_000_000.0) / calls : 0; + logger.info( + String.format( + Locale.US, + " %d. %s - %.1fs (%d calls, %.2fms/call)", + i + 1, + entry.getKey(), + seconds, + calls, + avgMs)); + } + } + } + + /** Record for per-class performance metrics. */ + private record ClassMetric( + String className, long elapsedNanos, int fieldCount, int collectionCount) {} + + private void reportPhase( + ProcessingLogger logger, String phase, double parentSeconds, String prefix, boolean isLast) { + long nanos = phaseTimes.getOrDefault(phase, 0L); + double seconds = nanos / 1_000_000_000.0; + double percentage = parentSeconds > 0 ? (seconds / parentSeconds) * 100 : 0; + String connector = isLast ? "└─ " : "├─ "; + logger.info( + String.format( + Locale.US, "%s%s%s: %.1fs (%.1f%%)", prefix, connector, phase, seconds, percentage)); + List children = PHASE_CHILDREN.get(phase); + if (children != null) { + String childPrefix = prefix + (isLast ? " " : "│ "); + for (int i = 0; i < children.size(); i++) { + reportPhase(logger, children.get(i), seconds, childPrefix, i == children.size() - 1); + } + } + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/NoOpPerformanceTracker.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/NoOpPerformanceTracker.java new file mode 100644 index 00000000..6a771d4a --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/NoOpPerformanceTracker.java @@ -0,0 +1,79 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.processing.logging; + +/** + * No-operation implementation of {@link PerformanceTracker} that does nothing. + * + *

All methods are empty, so the JIT compiler can eliminate them entirely when the tracker is + * fixed at construction time. This ensures zero overhead when performance tracking is disabled. + */ +public final class NoOpPerformanceTracker implements PerformanceTracker { + + @Override + public void startPhase(String phase, String className) { + // No-op + } + + @Override + public void endPhase(String phase) { + // No-op + } + + @Override + public void startGenerator(String generatorName) { + // No-op + } + + @Override + public void endGenerator(String generatorName) { + // No-op + } + + @Override + public void startEnhancer(String enhancerName) { + // No-op + } + + @Override + public void endEnhancer(String enhancerName) { + // No-op + } + + @Override + public void startClass(String className) { + // No-op + } + + @Override + public void endClass(int fieldCount, int collectionCount) { + // No-op + } + + @Override + public void generateReport(ProcessingLogger logger) { + // No-op + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PerformanceTracker.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PerformanceTracker.java new file mode 100644 index 00000000..7d154778 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PerformanceTracker.java @@ -0,0 +1,135 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.processing.logging; + +/** + * Interface for tracking performance metrics during annotation processing. + * + *

Implementations: + * + *

    + *
  • {@link NoOpPerformanceTracker} - all methods are no-ops, zero overhead (default) + *
  • {@link ActivePerformanceTracker} - actual measurement with nanoTime and aggregation + *
+ * + *

The No-Op pattern ensures that when performance tracking is disabled, the JIT compiler can + * eliminate all tracking calls entirely, as the receiver type is fixed at construction time and all + * methods are empty. + */ +public interface PerformanceTracker { + + // Top-level phases + String PHASE_CONFIGURATION_RESOLUTION = "Configuration Resolution"; + String PHASE_BUILDER_DEFINITION_EXTRACTION = "Builder Definition Extraction"; + String PHASE_DTO_MAPPING = "DTO Mapping"; + String PHASE_CODE_GENERATION = "Code Generation"; + + // Code Generation children + String PHASE_SOURCE_CONSTRUCTION = "Source Construction"; + String PHASE_FILE_WRITING = "File Writing"; + + // Source Construction children + String PHASE_ELEMENT_BUILDING = "Element Building"; + String PHASE_STRING_GENERATION = "String Generation"; + String PHASE_FORMATTING = "Formatting"; + + // Element Building children + String PHASE_CLASS_CREATION = "Class Creation"; + String PHASE_CLASS_METADATA = "Class Metadata"; + String PHASE_FIELDS = "Fields"; + String PHASE_CONSTRUCTORS = "Constructors"; + String PHASE_METHODS = "Methods"; + String PHASE_NESTED_TYPES = "Nested Types"; + String PHASE_CLASS_ANNOTATIONS = "Class Annotations"; + + /** + * Starts tracking a processing phase for a specific class. + * + * @param phase the phase identifier (e.g., "Configuration Resolution", "Builder Definition + * Extraction") + * @param className the simple name of the class being processed + */ + void startPhase(String phase, String className); + + /** + * Ends tracking a processing phase. + * + * @param phase the phase identifier that was started + */ + void endPhase(String phase); + + /** + * Starts tracking an individual method generator invocation. + * + * @param generatorName the simple class name of the method generator + */ + void startGenerator(String generatorName); + + /** + * Ends tracking an individual method generator invocation. + * + * @param generatorName the simple class name of the method generator that was started + */ + void endGenerator(String generatorName); + + /** + * Starts tracking an individual builder enhancer invocation. + * + * @param enhancerName the simple class name of the builder enhancer + */ + void startEnhancer(String enhancerName); + + /** + * Ends tracking an individual builder enhancer invocation. + * + * @param enhancerName the simple class name of the builder enhancer that was started + */ + void endEnhancer(String enhancerName); + + /** + * Records the start of processing for a specific class. + * + *

Call this before any work begins for the class. Field and collection counts are not yet + * known at this point; they are passed to {@link #endClass(int, int)} after extraction. + * + * @param className the simple name of the class being processed + */ + void startClass(String className); + + /** + * Records the end of processing for the current class. + * + * @param fieldCount the number of fields in the class + * @param collectionCount the number of collection-type fields + */ + void endClass(int fieldCount, int collectionCount); + + /** + * Generates and logs the performance report. + * + * @param logger the processing logger to output the report + */ + void generateReport(ProcessingLogger logger); +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java similarity index 98% rename from processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingLogger.java rename to processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java index 5be6032e..fa3bf1a5 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingLogger.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java @@ -22,12 +22,14 @@ * SOFTWARE. */ -package org.javahelpers.simple.builders.processor.processing; +package org.javahelpers.simple.builders.processor.processing.logging; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.tools.Diagnostic; +import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsEnum; +import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; /** * Logger for all messages during annotation processing. Providing util-functions for posting diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java index 5a01ba85..cd9caa34 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java @@ -49,7 +49,8 @@ import org.javahelpers.simple.builders.processor.model.method.MethodCodeDto; import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; import org.javahelpers.simple.builders.processor.model.type.TypeName; -import org.javahelpers.simple.builders.processor.processing.ProcessingLogger; +import org.javahelpers.simple.builders.processor.processing.logging.NoOpPerformanceTracker; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; import org.junit.jupiter.api.Test; /** @@ -85,7 +86,8 @@ void shouldWrapRenderingRuntimeExceptionInBuilderException() { classDef.addConstructor(constructor); ProcessingEnvironment env = new NoopProcessingEnvironment(); - RoasterCodeGenerator generator = new RoasterCodeGenerator(env, new ProcessingLogger(env)); + RoasterCodeGenerator generator = + new RoasterCodeGenerator(env, new ProcessingLogger(env), new NoOpPerformanceTracker()); BuilderException thrown = assertThrows(BuilderException.class, () -> generator.generateClass(classDef));