Timer Ninja is a lightweight, self-contained Java library that uses AspectJ AOP to track method execution times with hierarchical call tree visualization. The project follows these principles:
- Minimal: Only essential features, no bloat
- Self-contained: No external dependencies beyond Java/AspectJ/SLF4J
- Easy to use: Simple annotation-based API
- No external providers: Everything runs locally
Description: Automatically capture and log exceptions thrown by tracked methods.
Features:
- Include exception type, message, and stack trace in the trace output
- Mark failed methods distinctly in the call tree
- Helps correlate performance issues with error conditions
Example Output:
public void processPayment(User user, int amount) - 770 ms
|-- public boolean changeAmount(User user, int amount) - 306 ms
|-- public void notify(User user) - 258 ms ✗ [Exception: java.io.IOException: Failed to send email]
Implementation Complexity: Low
Description: Track how many times each method is invoked and provide statistics.
Features:
- Track invocation count for each method within a trace context
- Calculate min/max/average execution time for methods called multiple times
- Display statistics alongside individual call times
Example Output:
public void processBatch(List<User> users) - 5230 ms
|-- public void processUser(User user) - Called 50 times | Avg: 98ms | Min: 45ms | Max: 250ms
Implementation Complexity: Medium
Description: Add option to output traces in JSON format alongside the existing tree format.
Features:
- Structured JSON output for programmatic analysis
- Useful for integration with monitoring systems or dashboards
- Toggle via configuration (e.g.,
TimerNinjaConfiguration.getInstance().setJsonOutput(true))
Example Output:
{
"traceId": "c9ffeb39-3457-48d4-9b73-9ffe7d612165",
"timestamp": "2023-04-06T14:27:50.322Z",
"thread": "main",
"rootMethod": {
"name": "public User getUserById(int userId)",
"executionTime": 554,
"timeUnit": "MILLIS",
"children": [...]
}
}Implementation Complexity: Low
Description: Optional feature to log return values (similar to argument logging).
Features:
- Add
includeReturnValueparameter to@TimerNinjaTracker - Uses
toString()of return objects (same pattern as argument logging) - Helps understand the relationship between inputs, outputs, and execution time
Example Usage:
@TimerNinjaTracker(includeArgs = true, includeReturnValue = true)
public User getUserById(int userId) {
// Method logic
}Example Output:
public User getUserById(int userId) - Args: [userId={123}] - Returns: User{name='John', id=123} - 554 ms
Implementation Complexity: Low
Description: Enable/disable tracking for specific packages or classes via configuration.
Features:
- Exclude noisy third-party library calls from traces
- Include/exclude patterns using simple wildcards (e.g.,
com.myapp.*) - Configured via
TimerNinjaConfiguration
Example Usage:
TimerNinjaConfiguration config = TimerNinjaConfiguration.getInstance();
config.addExcludePackage("org.springframework.*");
config.addExcludeClass("com.myapp.util.Helper");Implementation Complexity: Low
Description: Generate a minimal report showing p90 and p95 execution times for tracked methods.
Features:
- Track execution times for each unique method signature across multiple invocations
- Calculate and display p90 and p95 percentiles on demand
- Simple, clean report format with minimal output
- Configurable history size to limit memory usage
- Manual report generation via configuration API
Example Report Output:
===== Timer Ninja Statistics Report =====
Total tracked methods: 3
1. public void processPayment(User user, int amount)
Invocations: 150 | p90: 780ms | p95: 920ms
2. public User findUser(int userId)
Invocations: 500 | p90: 250ms | p95: 300ms
3. public boolean changeAmount(User user, int amount)
Invocations: 150 | p90: 320ms | p95: 380ms
===== End of Report =====
Example Usage:
// Enable statistics tracking
TimerNinjaConfiguration config = TimerNinjaConfiguration.getInstance();
config.setTrackStatistics(true);
// Set maximum history per method (to limit memory)
config.setMaxHistorySize(1000);
// Print report on demand
config.printStatisticsReport();Implementation Details:
- Simple in-memory data structure: Map<MethodSignature, List>
- Percentile calculation: Sort times and apply standard percentile algorithm
- Memory-efficient: Configurable maximum history size per method
- Thread-safe: Use concurrent data structures for multi-threaded applications
Implementation Complexity: Medium
Description: Support threshold as a percentage of parent method's execution time.
Features:
- Show only children that take > X% of parent's time
- Example:
thresholdPercentage = 10(show only methods taking >10% of parent time) - Helps identify significant slowdowns in call chains
Example Usage:
@TimerNinjaTracker(thresholdPercentage = 10)
public void processOrder(Order order) {
// Only children taking >10% will be shown
}Implementation Complexity: Medium
Description: Track only a percentage of invocations randomly.
Features:
- Reduces overhead in production while providing statistical insights
- Configurable sampling rate (e.g., track 10% of calls)
- Can be set globally or per annotation
Example Usage:
@TimerNinjaTracker(samplingRate = 0.1) // Track 10% of calls
public void expensiveOperation() {
// Method logic
}Global Configuration:
TimerNinjaConfiguration.getInstance().setGlobalSamplingRate(0.2); // 20% samplingImplementation Complexity: Low
Description: Track and display percentile statistics (p50, p90, p95, p99) for methods called multiple times.
Features:
- More meaningful than simple average for performance analysis
- Helps identify outliers and consistency issues
- Configurable percentiles via annotation
Example Usage:
@TimerNinjaTracker(includeStats = true, percentiles = {50, 90, 95, 99})
public void processItem(Item item) {
// Method logic
}Example Output:
public void processItem(Item item) - Called 100 times | Avg: 50ms | p50: 45ms | p90: 75ms | p95: 90ms | p99: 120ms
Implementation Complexity: Medium
Description: Add optional label parameter to annotation for custom naming and grouping.
Features:
- Group similar operations or add business context
- Useful for filtering and aggregation
- Labels appear in trace output
Example Usage:
@TimerNinjaTracker(label = "API_CALL")
public User fetchUser(int userId) {
// Method logic
}
@TimerNinjaTracker(label = "API_CALL")
public Order fetchOrder(int orderId) {
// Method logic
}Implementation Complexity: Low
Description: Compare current execution time with historical average and flag significant slowdowns.
Features:
- Requires simple in-memory statistics tracking
- Configurable threshold for what constitutes a slowdown (e.g., >2x average)
- Helps detect performance regressions in development/testing
Example Usage:
@TimerNinjaTracker(detectSlowDown = true, slowDownThreshold = 2.0)
public void processData(Data data) {
// Method logic
}Example Output:
public void processData(Data data) - 500ms ⚠ [Slowdown detected! Avg: 200ms, Current: 2.5x slower]
Implementation Complexity: Medium
Description: Optional tracking of heap memory before/after method execution.
Features:
- Uses Java's built-in
Runtimeclass - Helps identify memory-intensive operations
- Displays memory delta in trace output
Example Usage:
@TimerNinjaTracker(trackMemory = true)
public void loadLargeData() {
// Method logic
}Example Output:
public void loadLargeData() - 1250ms | Memory: +15.2MB
Implementation Complexity: Low
Description: Better handling of CompletableFuture, ExecutorService, and other async patterns.
Features:
- Correlate async operations back to parent context
- Track parallel execution paths separately
- More complex but valuable for modern applications
Implementation Complexity: High
Description: Add file name and line number to tracked methods.
Features:
- Uses AspectJ's
getSourceLocation() - Helps locate code quickly in large projects
- Optional feature to keep output clean
Example Usage:
@TimerNinjaTracker(includeLocation = true)
public void process() {
// Method logic
}Example Output:
public void process() - [UserService.java:125] - 350ms
Implementation Complexity: Low
Description: Automatically stop tracking if trace exceeds a duration limit.
Features:
- Prevents excessive output in long-running operations
- Configurable via annotation or global setting
- Marks truncated traces in output
Example Usage:
@TimerNinjaTracker(maxTraceDuration = 5000) // Stop after 5 seconds
public void longRunningProcess() {
// Method logic
}Implementation Complexity: Low
Description: Limit how deep in the call stack to track.
Features:
- Prevents excessive nesting in recursive or deep call chains
- Configurable depth limit per annotation or globally
- Useful for preventing trace explosion
Example Usage:
@TimerNinjaTracker(maxDepth = 3)
public void process() {
// Only track 3 levels deep
}Global Configuration:
TimerNinjaConfiguration.getInstance().setMaxDepth(5);Implementation Complexity: Low
Given the minimal/self-contained requirement, I'd suggest implementing in this order:
- Exception Tracking - Immediately adds debugging value
- Return Value Logging - Complements existing argument logging
- Package/Class Filtering - Reduces noise immediately
- JSON Output Format - Enables programmatic analysis
- Execution Count & Statistics - Core performance insight
- Sampling Mode - Production-ready feature
- Custom Labels/Tags - Simple but useful for organization
- Dynamic Thresholds - Advanced filtering capability
- Percentile Metrics - Advanced performance analysis
- Slow-Down Detection - Regression detection
- Memory Usage Tracking - Additional dimension of analysis
- Code Location - Developer convenience
- Trace/Depth Limits - Safety mechanisms
- Async Support - Complex but valuable for modern apps
All features should adhere to these principles:
- Optional: Disabled by default to maintain minimalism
- Self-contained: No external dependencies beyond Java/AspectJ/SLF4J
- Backward Compatible: Existing code continues to work without changes
- Configurable: Can be controlled via annotation parameters or global
TimerNinjaConfiguration - Performance First: Features should not significantly impact application performance
- Clear Documentation: Each feature should be well-documented with examples
// Global configuration example
TimerNinjaConfiguration config = TimerNinjaConfiguration.getInstance();
// Enable/disable features
config.setJsonOutput(true);
config.setTrackMemory(true);
config.setDetectSlowDowns(true);
// Set global defaults
config.setDefaultThreshold(100);
config.setGlobalSamplingRate(0.1);
config.setMaxDepth(5);
// Filtering
config.addExcludePackage("org.springframework.*");
config.addExcludeClass("com.myapp.util.*");
// Logging
config.toggleSystemOutLog(true);
config.setLogLevel(LogLevel.DEBUG);These improvements would significantly enhance Timer Ninja's capabilities while maintaining its core philosophy of being minimal, self-contained, and easy to use. The features are prioritized by impact and complexity, allowing for incremental implementation that adds value at each stage.