Skip to content

Add datetime-aware time filter and select functions to JfrPath#87

Open
jbachorik wants to merge 12 commits intomainfrom
jb/timestamps
Open

Add datetime-aware time filter and select functions to JfrPath#87
jbachorik wants to merge 12 commits intomainfrom
jb/timestamps

Conversation

@jbachorik
Copy link
Collaborator

@jbachorik jbachorik commented Feb 25, 2026

Summary

  • Normalize timestamps at parse time: startTime and duration are converted from JFR ticks to epoch nanoseconds by MapValueBuilder/MapValueBuilderBaseline using the new Control.ChunkInfo.asEpochNanos() API, so all downstream query code works with wall-clock values
  • asDateTime([path][, format=...]): New pipeline operator and select() function that formats epoch-nanosecond fields as human-readable datetime strings
  • truncate(field, unit): New select() function that truncates an epoch-nanosecond value to a time boundary (second/minute/hour/day/week/month), enabling time-series groupBy
  • formatDuration(field): New pipeline operator and select() function that formats a nanosecond duration as a human-readable string (123ns, 4.56ms, 2h 15m 30s)
  • before(field, datetime) / after(field, datetime): New filter predicates for time-based filtering with datetime string parsing (ISO-8601, local datetime, date-only)
  • on(field, "yyyy-MM-dd"): New filter predicate that matches events on a specific calendar date (local timezone)
  • between() datetime support: Extended to accept datetime strings as bounds in addition to numbers
  • timerange() simplified: Removed chunk-metadata dependency; uses normalized epoch-nanos directly; output keys renamed from minTicks/maxTicks to minEpochNanos/maxEpochNanos
  • Documentation, completion, tests: JFRPath.md, FunctionRegistry.java, FunctionRegistryTest.java, and JfrPathDateTimeFunctionsTest.java updated for all new functions

Test plan

  • Run ./gradlew test to verify all existing and new tests pass
  • Verify tab completion suggests before, after, on, between, asDateTime, truncate, formatDuration
  • Test events/jdk.ExecutionSample[on(startTime, "yyyy-MM-dd")] with a real recording
  • Test time-series bucketing: events/jdk.ExecutionSample | select(asDateTime(truncate(startTime, "minute")) as bucket) | groupBy(bucket, agg=count)
  • Test events/jdk.JavaMonitorEnter | select(startTime, formatDuration(duration) as dur)
  • Test formatDuration as pipeline op: events/jdk.JavaMonitorEnter/duration | formatDuration()

🤖 Generated with Claude Code

jbachorik and others added 5 commits February 25, 2026 21:22
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fore/after/between arg handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jbachorik jbachorik added the AI AI-generated code or contributions label Feb 25, 2026
@github-actions
Copy link

github-actions bot commented Feb 25, 2026

Combined JUnit Test Report

  • Total: 1226
  • Passed: 1222
  • Failures: 0
  • Errors: 0
  • Skipped: 4

HTML Test Reports

Run artifacts: https://github.com/btraceio/jafar/actions/runs/22456165320

jbachorik and others added 2 commits February 25, 2026 22:40
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jbachorik jbachorik requested a review from Copilot February 26, 2026 07:44
@jbachorik jbachorik marked this pull request as ready for review February 26, 2026 07:44
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds comprehensive datetime-aware time filtering and formatting capabilities to JfrPath, fundamentally changing how timestamps are handled throughout the system. The key architectural change is normalizing timestamps from JFR ticks to epoch nanoseconds at parse time, simplifying all downstream time operations.

Changes:

  • Timestamps normalized at parse time: startTime and duration fields converted from JFR ticks to epoch nanoseconds by value builders, enabling consistent time handling
  • New time filter predicates: before(), after(), on(), and extended between() to accept datetime strings in addition to numeric values
  • New datetime functions: asDateTime() for formatting timestamps, truncate() for time-series bucketing, and formatDuration() for human-readable duration display
  • Simplified timerange() operator: removed chunk metadata dependency, outputs now use minEpochNanos/maxEpochNanos instead of minTicks/maxTicks
  • Comprehensive test coverage and documentation updates

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated no comments.

Show a summary per file
File Description
parser-core/src/main/java/io/jafar/parser/impl/MapValueBuilder.java Adds timestamp normalization in onComplexValueEnd to convert ticks to epoch nanos
parser-core/src/main/java/io/jafar/parser/impl/MapValueBuilderBaseline.java Adds timestamp normalization for baseline implementation
parser-core/src/main/java/io/jafar/parser/impl/LazyMapValueBuilder.java Adds timestamp normalization for lazy map implementation
parser-core/src/main/java/io/jafar/parser/impl/ChunkInfoImpl.java Implements asEpochNanos() method to support timestamp conversion
parser-core/src/main/java/io/jafar/parser/api/Control.java Adds asEpochNanos() API to ChunkInfo interface
parser-core/src/test/java/io/jafar/parser/UntypedJafarParserTest.java Updates test to use normalized epoch nanos directly
jfr-shell-core/src/main/java/io/jafar/shell/jfrpath/JfrPathEvaluator.java Implements new filter predicates, datetime functions, and simplified timerange
jfr-shell-core/src/main/java/io/jafar/shell/jfrpath/JfrPathParser.java Adds parsing support for asDateTime and formatDuration operators
jfr-shell-core/src/main/java/io/jafar/shell/jfrpath/JfrPath.java Defines AsDateTimeOp and FormatDurationOp pipeline operator classes
jfr-shell-core/src/test/java/io/jafar/shell/jfrpath/JfrPathTimeRangeTest.java Updates tests for renamed output keys (minTicks → minEpochNanos)
jfr-shell-core/src/test/java/io/jafar/shell/jfrpath/JfrPathDateTimeFunctionsTest.java Comprehensive test coverage for all new datetime functions
jfr-shell/src/main/java/io/jafar/shell/cli/completion/FunctionRegistry.java Registers new functions and updates between() parameter types
jfr-shell/src/test/java/io/jafar/shell/cli/completion/FunctionRegistryTest.java Updates tests for new functions and changed parameter types
doc/cli/JFRPath.md Documents all new time filter functions and datetime operations with examples
Comments suppressed due to low confidence (6)

jfr-shell-core/src/main/java/io/jafar/shell/jfrpath/JfrPathEvaluator.java:954

  • The getFormatter method calls DateTimeFormatter.ofPattern(format) which can throw IllegalArgumentException if the format string is invalid. This exception will propagate to the user without a clear error message indicating that the datetime format pattern is invalid.

Consider wrapping this in a try-catch block to provide a more user-friendly error message that clearly indicates the issue is with the format string, e.g., "Invalid datetime format pattern: '{format}'".

  private static DateTimeFormatter getFormatter(String format) {
    if (format == null || format.isEmpty()) {
      return DateTimeFormatter.ISO_LOCAL_DATE_TIME;
    }
    return DateTimeFormatter.ofPattern(format);
  }

jfr-shell-core/src/main/java/io/jafar/shell/jfrpath/JfrPathEvaluator.java:1172

  • The truncate function with "week" unit uses previousOrSame(DayOfWeek.MONDAY) which assumes weeks start on Monday. This is ISO-8601 compliant, but may not match user expectations in locales where weeks start on Sunday (e.g., US). Consider documenting this behavior explicitly in the JFRPath.md documentation, or adding a note that week truncation follows ISO-8601 (Monday as first day of week).
          case "week" ->
              zdt.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
                  .truncatedTo(ChronoUnit.DAYS);

jfr-shell-core/src/main/java/io/jafar/shell/jfrpath/JfrPathEvaluator.java:2245

  • The on filter calls LocalDate.parse() which can throw DateTimeParseException if the date string is not in the expected "yyyy-MM-dd" format. This exception will propagate to the user without a clear error message.

Consider wrapping this in a try-catch block to provide a more user-friendly error message, e.g., "Invalid date format for on() filter. Expected yyyy-MM-dd, got: '{value}'".

          LocalDate date = LocalDate.parse(String.valueOf(resolveArg(root, args.get(1))));

jfr-shell-core/src/main/java/io/jafar/shell/jfrpath/JfrPathEvaluator.java:2217

  • The between function implementation has an issue when mixing numeric and datetime string bounds. Currently, if both bounds are strings, it uses long comparison with parseEpochNanos. However, if only one bound is a string (e.g., between(startTime, 1000, "2024-08-13")), it falls through to the double comparison path which would incorrectly interpret the datetime string as a number via toDouble(). This could lead to parsing errors or incorrect comparisons.

Consider handling the case where either (but not both) bounds are strings, or document that both bounds must be of the same type (both strings or both numbers).

        case "between" -> {
          ensureArgs(args, 3);
          Object v = resolveArg(root, args.get(0));
          if (!(v instanceof Number betweenNum)) return false;
          Object loArg = resolveArg(root, args.get(1));
          Object hiArg = resolveArg(root, args.get(2));
          if (loArg instanceof String && hiArg instanceof String) {
            long x = betweenNum.longValue();
            return x >= parseEpochNanos(String.valueOf(loArg))
                && x <= parseEpochNanos(String.valueOf(hiArg));
          }
          double x = betweenNum.doubleValue();
          return x >= toDouble(loArg) && x <= toDouble(hiArg);
        }

jfr-shell-core/src/main/java/io/jafar/shell/jfrpath/JfrPathEvaluator.java:947

  • The formatDuration method doesn't handle negative duration values. If a negative nanosecond value is passed (which could happen due to data corruption or incorrect calculation), the method will produce incorrect output. For example, negative values less than -1000 will fall through to the first condition and be formatted as "-500ns", but larger negative values could produce confusing output like "-5m -30s".

Consider adding a check at the beginning of the method to handle negative values explicitly, either by returning a special string like "invalid" or by formatting them with a negative sign prefix.

  private static String formatDuration(long nanos) {
    if (nanos < 1_000) {
      return nanos + "ns";
    } else if (nanos < 1_000_000) {
      return String.format("%.2fus", nanos / 1_000.0);
    } else if (nanos < 1_000_000_000) {
      return String.format("%.2fms", nanos / 1_000_000.0);
    } else if (nanos < 60_000_000_000L) {
      return String.format("%.2fs", nanos / 1_000_000_000.0);
    } else if (nanos < 3_600_000_000_000L) {
      long totalSecs = nanos / 1_000_000_000L;
      long mins = totalSecs / 60;
      long secs = totalSecs % 60;
      return String.format("%dm %ds", mins, secs);
    } else {
      long totalSecs = nanos / 1_000_000_000L;
      long hours = totalSecs / 3600;
      long mins = (totalSecs % 3600) / 60;
      long secs = totalSecs % 60;
      return String.format("%dh %dm %ds", hours, mins, secs);
    }
  }

jfr-shell-core/src/main/java/io/jafar/shell/jfrpath/JfrPathEvaluator.java:2303

  • Potential overflow issue when converting Instant to epoch nanoseconds. The calculation i.getEpochSecond() * 1_000_000_000L + i.getNano() can overflow for dates beyond year 2262 (when epoch seconds exceed Long.MAX_VALUE / 1_000_000_000). While JFR recordings are unlikely to have such far-future timestamps, parsing user-supplied datetime strings in filters like before(), after(), or between() could potentially trigger this if invalid dates are provided.

Consider adding overflow checks or catching ArithmeticException to provide a clearer error message, or document the supported date range.

  private static long parseEpochNanos(String s) {
    // ISO instant with timezone offset, e.g. "2024-08-13T16:24:00Z"
    try {
      Instant i = Instant.parse(s);
      return i.getEpochSecond() * 1_000_000_000L + i.getNano();
    } catch (DateTimeParseException ignored) {
    }
    // Local date-time without timezone, e.g. "2024-08-13T16:24:00"
    try {
      Instant i = LocalDateTime.parse(s).atZone(ZoneId.systemDefault()).toInstant();
      return i.getEpochSecond() * 1_000_000_000L + i.getNano();
    } catch (DateTimeParseException ignored) {
    }
    // Date only, e.g. "2024-08-13" — treated as start of day in local zone
    try {
      Instant i = LocalDate.parse(s).atStartOfDay(ZoneId.systemDefault()).toInstant();
      return i.getEpochSecond() * 1_000_000_000L + i.getNano();
    } catch (DateTimeParseException ignored) {
    }
    throw new IllegalArgumentException("Cannot parse datetime: " + s);
  }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

jbachorik and others added 5 commits February 26, 2026 18:33
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI AI-generated code or contributions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants