@@ -428,8 +428,9 @@ public Response getEventRate(
428428 description =
429429 "Returns a histogram of frequencies for intervals between decay events."
430430 + " Requires at least 100 intervals for meaningful statistical analysis."
431- + " Default bucket size (100ns) is optimized for radioactive decay"
432- + " intervals in the 2-10µs range." )
431+ + " When bucketSizeNs is omitted (or set to -1) the server auto-selects"
432+ + " a bucket size that targets approximately 64 buckets across the"
433+ + " observed interval range, rounded to a 1/2/5 × 10^k value." )
433434 @ APIResponses ({
434435 @ APIResponse (
435436 responseCode = "200" ,
@@ -442,19 +443,21 @@ public Response getIntervalHistogram(
442443 @ Parameter (description = "Start of time window (ISO-8601)" ) @ QueryParam ("from" )
443444 String from ,
444445 @ Parameter (description = "End of time window (ISO-8601)" ) @ QueryParam ("to" ) String to ,
445- @ Parameter (description = "Bucket size in nanoseconds (must be positive, default: 100)" )
446+ @ Parameter (
447+ description =
448+ "Bucket size in nanoseconds. Pass -1 (or omit) for"
449+ + " auto-sizing based on the observed interval range." )
446450 @ QueryParam ("bucketSizeNs" )
447- @ DefaultValue ("100 " )
451+ @ DefaultValue ("-1 " )
448452 int bucketSizeNs ) {
449453
450454 LOG .debugf (
451455 "Interval histogram request: from=%s, to=%s, bucketSize=%d" ,
452456 from , to , bucketSizeNs );
453457
454- // Validate bucket size to prevent division by zero and negative values
455- if (bucketSizeNs <= 0 ) {
458+ if (bucketSizeNs != -1 && bucketSizeNs <= 0 ) {
456459 throw ValidationException .invalidParameter (
457- "bucketSizeNs" , bucketSizeNs , "positive integer" );
460+ "bucketSizeNs" , bucketSizeNs , "positive integer or -1 for auto " );
458461 }
459462
460463 TimeWindow window = parseTimeWindow (from , to );
@@ -464,11 +467,19 @@ public Response getIntervalHistogram(
464467 throw ValidationException .insufficientData ("intervals" , 100 , intervals .size ());
465468 }
466469
470+ int effectiveBucketSizeNs = bucketSizeNs ;
471+ if (effectiveBucketSizeNs == -1 ) {
472+ effectiveBucketSizeNs = autoBucketSizeNs (intervals );
473+ LOG .debugf (
474+ "Auto-selected bucketSizeNs=%d for %d intervals" ,
475+ effectiveBucketSizeNs , intervals .size ());
476+ }
477+
467478 Map <Long , Integer > histogram =
468- entropyStatisticsService .createHistogram (intervals , bucketSizeNs );
479+ entropyStatisticsService .createHistogram (intervals , effectiveBucketSizeNs );
469480 IntervalHistogramDTO response =
470481 IntervalHistogramDTO .from (
471- histogram , intervals , bucketSizeNs , window .start , window .end );
482+ histogram , intervals , effectiveBucketSizeNs , window .start , window .end );
472483
473484 LOG .infof (
474485 "Histogram: %d buckets from %d intervals (min=%dns, max=%dns)" ,
@@ -551,4 +562,45 @@ public Response getFilterOptions() {
551562
552563 return Response .ok (new EventFilterOptionsDTO (batchIds , channels )).build ();
553564 }
565+
566+ /**
567+ * Picks a bucket size that yields roughly {@link #AUTO_HISTOGRAM_TARGET_BUCKETS} buckets
568+ * across the observed interval range, snapped to a 1/2/5 × 10^k mantissa so bucket edges
569+ * are human-readable. Floors at {@link #AUTO_HISTOGRAM_MIN_BUCKET_NS} so fine-grained
570+ * entropy sources do not collapse into a single bucket.
571+ */
572+ static int autoBucketSizeNs (List <Long > intervals ) {
573+ long min = Long .MAX_VALUE ;
574+ long max = Long .MIN_VALUE ;
575+ for (long v : intervals ) {
576+ if (v < min ) min = v ;
577+ if (v > max ) max = v ;
578+ }
579+ long range = Math .max (1L , max - min );
580+ double ideal = (double ) range / AUTO_HISTOGRAM_TARGET_BUCKETS ;
581+ if (ideal < AUTO_HISTOGRAM_MIN_BUCKET_NS ) {
582+ return AUTO_HISTOGRAM_MIN_BUCKET_NS ;
583+ }
584+
585+ // Snap ``ideal`` up to the next value in the 1/2/5 × 10^k series.
586+ double exponent = Math .floor (Math .log10 (ideal ));
587+ double pow = Math .pow (10.0 , exponent );
588+ double mantissa = ideal / pow ;
589+ double snapped ;
590+ if (mantissa <= 1.0 ) {
591+ snapped = 1.0 * pow ;
592+ } else if (mantissa <= 2.0 ) {
593+ snapped = 2.0 * pow ;
594+ } else if (mantissa <= 5.0 ) {
595+ snapped = 5.0 * pow ;
596+ } else {
597+ snapped = 10.0 * pow ;
598+ }
599+
600+ long bucket = Math .max (AUTO_HISTOGRAM_MIN_BUCKET_NS , (long ) snapped );
601+ return (int ) Math .min (bucket , Integer .MAX_VALUE );
602+ }
603+
604+ private static final int AUTO_HISTOGRAM_TARGET_BUCKETS = 64 ;
605+ private static final int AUTO_HISTOGRAM_MIN_BUCKET_NS = 100 ;
554606}
0 commit comments