001/* 002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.benchmarking.common.metrics; 017 018import com.google.gson.JsonObject; 019import com.google.gson.JsonParseException; 020import com.google.gson.JsonParser; 021import de.cuioss.tools.logging.CuiLogger; 022 023import java.io.IOException; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import java.time.Instant; 027import java.util.ArrayList; 028import java.util.Collections; 029import java.util.List; 030 031import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.INFO.ITERATION_WINDOWS_PARSED; 032import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.WARN.FAILED_PARSE_TIMESTAMP_LINE; 033import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.WARN.TIMESTAMP_FILE_NOT_EXIST; 034 035/** 036 * Parses JMH iteration timestamp files to extract precise timing windows 037 * for Prometheus metrics collection. 038 * <p> 039 * Supports JSONL format (one JSON object per line) from TimestampProfiler. 040 */ 041public class IterationTimestampParser { 042 043 private static final CuiLogger LOGGER = new CuiLogger(IterationTimestampParser.class); 044 045 /** 046 * Represents a time window for a benchmark iteration 047 */ 048 public record IterationWindow( 049 String benchmarkName, 050 String forkId, 051 String iterationType, 052 int iterationNumber, 053 Instant startTime, 054 Instant endTime, 055 long durationMs 056 ) { 057 public boolean isMeasurement() { 058 return "MEASUREMENT".equalsIgnoreCase(iterationType); 059 } 060 061 public boolean isWarmup() { 062 return "WARMUP".equalsIgnoreCase(iterationType); 063 } 064 } 065 066 /** 067 * Parses JSONL timestamp file (one JSON object per line) 068 */ 069 public static List<IterationWindow> parseJsonlFile(Path timestampFile) throws IOException { 070 if (!Files.exists(timestampFile)) { 071 LOGGER.warn(TIMESTAMP_FILE_NOT_EXIST, timestampFile); 072 return Collections.emptyList(); 073 } 074 075 List<IterationWindow> windows = new ArrayList<>(); 076 077 try (var lines = Files.lines(timestampFile)) { 078 lines.forEach(line -> { 079 if (line.trim().isEmpty()) return; 080 081 try { 082 JsonObject node = JsonParser.parseString(line).getAsJsonObject(); 083 windows.add(parseIterationNode(node)); 084 } catch (JsonParseException | IllegalStateException e) { 085 // JsonParseException: malformed JSON 086 // IllegalStateException: not a JSON object (getAsJsonObject on wrong type) 087 LOGGER.warn(e, FAILED_PARSE_TIMESTAMP_LINE, line); 088 } 089 }); 090 } 091 092 LOGGER.info(ITERATION_WINDOWS_PARSED, windows.size(), timestampFile); 093 return windows; 094 } 095 096 /** 097 * Parses a single iteration node 098 */ 099 private static IterationWindow parseIterationNode(JsonObject node) { 100 return parseIterationNode(node, node.get("benchmark").getAsString()); 101 } 102 103 /** 104 * Parses a single iteration node with explicit benchmark name 105 */ 106 private static IterationWindow parseIterationNode(JsonObject node, String benchmarkName) { 107 return new IterationWindow( 108 benchmarkName, 109 node.get("fork").getAsString(), 110 node.get("type").getAsString(), 111 node.get("iteration").getAsInt(), 112 Instant.parse(node.get("start").getAsString()), 113 Instant.parse(node.get("end").getAsString()), 114 node.get("duration_ms").getAsLong() 115 ); 116 } 117 118}