Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
477 changes: 477 additions & 0 deletions FEATURE_RECOMMENDATIONS.md

Large diffs are not rendered by default.

606 changes: 606 additions & 0 deletions STATISTICS_FEATURE_PLAN.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@
/**
* Configuration class for code block tracking in {@link TimerNinjaBlock}.
* <p>
* This class provides a builder-style API to configure how a code block should be tracked,
* This class provides a builder-style API to configure how a code block should
* be tracked,
* similar to the options available in {@link TimerNinjaTracker} annotation.
* </p>
* <p>
* Example usage:
*
* <pre>
* BlockTrackerConfig config = new BlockTrackerConfig()
* .setTimeUnit(ChronoUnit.SECONDS)
* .setEnabled(true)
* .setThreshold(5);
* .setTimeUnit(ChronoUnit.SECONDS)
* .setEnabled(true)
* .setThreshold(5)
* .setTrackerId("my-custom-tracker");
*
* TimerNinjaBlock.measure("my block", config, () -> {
* TimerNinjaBlock.measure("my block", config, () -&gt; {
* // code to track
* });
* </pre>
Expand All @@ -31,23 +34,32 @@ public class BlockTrackerConfig {

/**
* Determine if this tracker should be active.
* Set to false will disable this tracker from the overall tracking trace result.
* Set to false will disable this tracker from the overall tracking trace
* result.
*/
private boolean enabled = true;

/**
* Set the threshold of the tracker. If the execution time of the code block is less than
* Set the threshold of the tracker. If the execution time of the code block is
* less than
* the threshold, the tracker will not be included in the tracking result.
* Default is 0, which means no threshold is set.
*/
private int threshold = 0;

/**
* Unique identifier for this block tracker in statistics.
* If null, defaults to "Block:blockName".
*/
private String trackerId = null;

/**
* Default constructor creating a config with default values:
* <ul>
* <li>timeUnit: MILLIS</li>
* <li>enabled: true</li>
* <li>threshold: 0 (no threshold)</li>
* <li>timeUnit: MILLIS</li>
* <li>enabled: true</li>
* <li>threshold: 0 (no threshold)</li>
* <li>trackerId: null (will use default)</li>
* </ul>
*/
public BlockTrackerConfig() {
Expand Down Expand Up @@ -80,7 +92,8 @@ public BlockTrackerConfig setEnabled(boolean enabled) {

/**
* Sets the threshold for execution time.
* Code blocks with execution time less than this threshold will not be included in the trace.
* Code blocks with execution time less than this threshold will not be included
* in the trace.
*
* @param threshold the threshold value in the configured time unit.
* Set to 0 or negative to disable threshold filtering.
Expand All @@ -91,6 +104,18 @@ public BlockTrackerConfig setThreshold(int threshold) {
return this;
}

/**
* Sets a custom tracker ID for statistics.
* If not set, defaults to "Block:blockName".
*
* @param trackerId the custom tracker ID
* @return this config instance for method chaining
*/
public BlockTrackerConfig setTrackerId(String trackerId) {
this.trackerId = trackerId;
return this;
}

/**
* Gets the configured time unit.
*
Expand Down Expand Up @@ -118,6 +143,15 @@ public int getThreshold() {
return threshold;
}

/**
* Gets the custom tracker ID.
*
* @return the tracker ID, or null if not set
*/
public String getTrackerId() {
return trackerId;
}

/**
* Creates a copy of this configuration.
*
Expand All @@ -128,6 +162,7 @@ public BlockTrackerConfig copy() {
copy.timeUnit = this.timeUnit;
copy.enabled = this.enabled;
copy.threshold = this.threshold;
copy.trackerId = this.trackerId;
return copy;
}

Expand All @@ -137,6 +172,7 @@ public String toString() {
"timeUnit=" + timeUnit +
", enabled=" + enabled +
", threshold=" + threshold +
", trackerId='" + trackerId + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package io.github.thanglequoc.timerninja;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* Statistics holder for a single tracker (identified by unique trackerId).
* <p>
* Stores execution time samples in a circular buffer with FIFO eviction.
* All statistical calculations (average, percentiles) are performed on-demand
* to minimize performance overhead during recording.
*/
public class MethodStatistics {

private final String trackerId;
private final String methodSignature;
private final String className;
private final int maxBufferSize;

// Circular buffer for execution times (in milliseconds)
private final List<Long> executionTimes;

// Threshold tracking
private int thresholdMs = -1;
private int thresholdExceededCount = 0;
private int thresholdWithinCount = 0;

// Parent-child hierarchy
private String parentTrackerId;
private final List<String> childTrackerIds;

/**
* Creates a new MethodStatistics instance.
*
* @param trackerId unique identifier for this tracker
* @param className fully qualified class name
* @param methodSignature method signature (shortened or full)
* @param maxBufferSize maximum number of samples to keep
*/
public MethodStatistics(String trackerId, String className, String methodSignature, int maxBufferSize) {
this.trackerId = trackerId;
this.className = className;
this.methodSignature = methodSignature;
this.maxBufferSize = maxBufferSize;
this.executionTimes = new ArrayList<>();
this.childTrackerIds = new ArrayList<>();
}

/**
* Records an execution time sample.
* If the buffer is full, the oldest entry is removed (FIFO eviction).
*
* @param executionTimeMs execution time in milliseconds
* @param thresholdMs threshold value (-1 if not set)
*/
public synchronized void recordExecution(long executionTimeMs, int thresholdMs) {
// FIFO eviction if buffer is full
if (executionTimes.size() >= maxBufferSize) {
executionTimes.remove(0);
}
executionTimes.add(executionTimeMs);

// Update threshold tracking
if (thresholdMs > 0) {
this.thresholdMs = thresholdMs;
if (executionTimeMs > thresholdMs) {
thresholdExceededCount++;
} else {
thresholdWithinCount++;
}
}
}

/**
* Returns the total number of recorded invocations.
* Note: This may be greater than the buffer size if eviction has occurred.
*
* @return total invocation count
*/
public int getInvocationCount() {
return thresholdExceededCount + thresholdWithinCount +
(thresholdMs <= 0 ? executionTimes.size() : 0);
}

/**
* Returns the number of samples currently in the buffer.
*
* @return sample count
*/
public synchronized int getSampleCount() {
return executionTimes.size();
}

/**
* Calculates the average execution time on-demand.
*
* @return average execution time in milliseconds, or 0 if no samples
*/
public synchronized long calculateAverage() {
if (executionTimes.isEmpty()) {
return 0;
}
long sum = 0;
for (Long time : executionTimes) {
sum += time;
}
return sum / executionTimes.size();
}

/**
* Calculates a percentile value on-demand.
*
* @param percentile the percentile to calculate (e.g., 50, 90, 95)
* @return the percentile value in milliseconds, or 0 if no samples
*/
public synchronized long calculatePercentile(int percentile) {
if (executionTimes.isEmpty()) {
return 0;
}
if (percentile < 0 || percentile > 100) {
throw new IllegalArgumentException("Percentile must be between 0 and 100");
}

List<Long> sorted = new ArrayList<>(executionTimes);
Collections.sort(sorted);

int index = (int) Math.ceil((percentile / 100.0) * sorted.size()) - 1;
index = Math.max(0, Math.min(index, sorted.size() - 1));
return sorted.get(index);
}

/**
* Returns the minimum execution time.
*
* @return minimum time in milliseconds, or 0 if no samples
*/
public synchronized long getMin() {
if (executionTimes.isEmpty()) {
return 0;
}
long min = Long.MAX_VALUE;
for (Long time : executionTimes) {
if (time < min) {
min = time;
}
}
return min;
}

/**
* Returns the maximum execution time.
*
* @return maximum time in milliseconds, or 0 if no samples
*/
public synchronized long getMax() {
if (executionTimes.isEmpty()) {
return 0;
}
long max = Long.MIN_VALUE;
for (Long time : executionTimes) {
if (time > max) {
max = time;
}
}
return max;
}

/**
* Resets all statistics for this tracker.
*/
public synchronized void reset() {
executionTimes.clear();
thresholdExceededCount = 0;
thresholdWithinCount = 0;
childTrackerIds.clear();
}

// --- Getters ---

public String getTrackerId() {
return trackerId;
}

public String getMethodSignature() {
return methodSignature;
}

public String getClassName() {
return className;
}

public int getThresholdMs() {
return thresholdMs;
}

public int getThresholdExceededCount() {
return thresholdExceededCount;
}

public int getThresholdWithinCount() {
return thresholdWithinCount;
}

public String getParentTrackerId() {
return parentTrackerId;
}

public void setParentTrackerId(String parentTrackerId) {
this.parentTrackerId = parentTrackerId;
}

public List<String> getChildTrackerIds() {
return new ArrayList<>(childTrackerIds);
}

public synchronized void addChildTrackerId(String childTrackerId) {
if (!childTrackerIds.contains(childTrackerId)) {
childTrackerIds.add(childTrackerId);
}
}

/**
* Returns a copy of the execution times buffer.
* Useful for testing and debugging.
*
* @return copy of execution times list
*/
public synchronized List<Long> getExecutionTimesCopy() {
return new ArrayList<>(executionTimes);
}
}
Loading