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}