All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.moditect.jfrunit.JfrEvents Maven / Gradle / Ivy

The newest version!
/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2020 - 2021 The JfrUnit authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.moditect.jfrunit;

import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.moditect.jfrunit.EnableEvent.StacktracePolicy;
import org.moditect.jfrunit.internal.EventConfiguration;
import org.moditect.jfrunit.internal.SyncEvent;

import jdk.jfr.Configuration;
import jdk.jfr.EventSettings;
import jdk.jfr.EventType;
import jdk.jfr.FlightRecorder;
import jdk.jfr.Recording;
import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordingStream;

public class JfrEvents {

    private static final Logger LOGGER = System.getLogger(JfrEvents.class.getName());

    private static final long INTERNAL_WAIT_TIME = 97;

    private Method testMethod;
    private String dumpFileName;
    private Queue events = new ConcurrentLinkedQueue<>();
    private AtomicLong sequence = new AtomicLong();
    private AtomicLong watermark = new AtomicLong();
    private RecordingStream stream;
    private Recording recording;
    private boolean capturing;

    public JfrEvents() {
    }

    void startRecordingEvents(String configurationName, List enabledEvents, Method testMethod, String dumpFileName) {
        if (configurationName != null && !enabledEvents.isEmpty()) {
            throw new IllegalArgumentException("Either @EnableConfiguration or @EnableEvent may be given, but not both at the same time");
        }

        LOGGER.log(Level.INFO, "Starting recording");

        CountDownLatch streamStarted = new CountDownLatch(1);

        List allEnabledEventTypes = matchEventTypes(enabledEvents);

        try {
            this.testMethod = testMethod;
            this.dumpFileName = dumpFileName;
            stream = startRecordingStream(configurationName, allEnabledEventTypes, streamStarted);
            recording = startRecording(configurationName, allEnabledEventTypes);

            awaitStreamStart(streamStarted);
            capturing = true;
            LOGGER.log(Level.INFO, "Event stream started");
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    void stopRecordingEvents() {
        try {
            URI testSourceUri = testMethod.getDeclaringClass().getProtectionDomain().getCodeSource().getLocation().toURI();
            Path dumpDir;
            try {
                dumpDir = Files.createDirectories(Path.of(testSourceUri).getParent().resolve("jfrunit"));
            }
            catch (FileSystemNotFoundException e) {
                dumpDir = Files.createTempDirectory(null);
                LOGGER.log(Level.WARNING, "'" + testSourceUri.getScheme() + "' is not a valid file system, dumping recording to a temporary location.");
            }

            String fileName = getDumpFileName();
            Path recordingPath = dumpDir.resolve(fileName);

            LOGGER.log(Level.INFO, "Stop recording: " + recordingPath);
            capturing = false;
            recording.stop();
            try {
                recording.dump(recordingPath);
            }
            catch (IOException ex) {
                LOGGER.log(Level.WARNING, "Could not dump to: " + recordingPath, ex);
                String defaultFileName = getDefaultDumpFileName();
                if (!defaultFileName.equals(fileName)) {
                    // perhaps the FS was not able to handle special characters
                    recordingPath = dumpDir.resolve(defaultFileName);
                    LOGGER.log(Level.INFO, "Retrying dump: " + recordingPath);
                    recording.dump(recordingPath);
                }
                else {
                    throw ex;
                }
            }
            recording.close();

            stream.close();
        }
        catch (IOException | URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Ensures all previously emitted events have been consumed.
     */
    public void awaitEvents() {
        SyncEvent event = new SyncEvent();
        event.begin();
        long seq = sequence.incrementAndGet();
        event.sequence = seq;
        event.cause = "awaiting events";
        event.commit();

        while (watermark.get() < seq) {
            try {
                Thread.sleep(INTERNAL_WAIT_TIME);
            }
            catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public void reset() {
        awaitEvents();
        events.clear();
    }

    public Stream events() {
        return stream();
    }

    public Stream filter(Predicate predicate) {
        return stream().filter(predicate);
    }

    public Stream filter(JfrEventType jfrEventType) {
        Stream result = null;

        try {
            result = stream()
                    .filter(recordedEvent -> jfrEventType.getName().equals(recordedEvent.getEventType().getName()))
                    .filter(jfrEventType.getPredicates().stream().map(JfrPredicate::getPredicate).reduce(x -> true, Predicate::and));
        }
        finally {
            jfrEventType.getPredicates().clear();
        }

        return result;
    }

    private Stream stream() {
        // avoid blocking when called outside of a test such as new JfrEvents().stream()
        if (capturing) {
            awaitEvents();
        }
        return events.stream();
    }

    private void awaitStreamStart(CountDownLatch streamStarted) throws InterruptedException {
        while (streamStarted.getCount() != 0) {
            SyncEvent event = new SyncEvent();
            event.sequence = sequence.incrementAndGet();
            event.cause = "awaiting stream start";
            event.begin();
            event.commit();
            Thread.sleep(INTERNAL_WAIT_TIME);
        }
    }

    private Recording startRecording(String configurationName, List enabledEvents) throws Exception {
        Recording recording;

        if (configurationName != null) {
            recording = new Recording(Configuration.getConfiguration(configurationName));
        }
        else {
            recording = new Recording();
            for (EventConfiguration enabledEvent : enabledEvents) {
                EventSettings settings = recording.enable(enabledEvent.name);
                if (enabledEvent.stackTrace == StacktracePolicy.INCLUDED) {
                    settings.withStackTrace();
                }
                else if (enabledEvent.stackTrace == StacktracePolicy.EXCLUDED) {
                    settings.withoutStackTrace();
                }

                if (enabledEvent.threshold != -1) {
                    settings.withThreshold(Duration.ofMillis(enabledEvent.threshold));
                }

                if (enabledEvent.period != -1) {
                    settings.withPeriod(Duration.ofMillis(enabledEvent.period));
                }
            }
        }

        recording.enable(SyncEvent.JFRUNIT_SYNC_EVENT_NAME);

        recording.start();
        return recording;
    }

    private RecordingStream startRecordingStream(String configurationName, List enabledEvents, CountDownLatch streamStarted) throws Exception {
        RecordingStream stream;

        if (configurationName != null) {
            stream = new RecordingStream(Configuration.getConfiguration(configurationName));
        }
        else {
            stream = new RecordingStream();
            for (EventConfiguration enabledEvent : enabledEvents) {
                EventSettings settings = stream.enable(enabledEvent.name);
                if (enabledEvent.stackTrace == StacktracePolicy.INCLUDED) {
                    settings.withStackTrace();
                }
                else if (enabledEvent.stackTrace == StacktracePolicy.EXCLUDED) {
                    settings.withoutStackTrace();
                }

                if (enabledEvent.threshold != -1) {
                    settings.withThreshold(Duration.ofMillis(enabledEvent.threshold));
                }
            }
        }

        // we need this as we keep reference to events - otherwise they could be changed after the capture
        // see RecordingStream#setReuse (the default is true)
        stream.setReuse(false);
        stream.enable(SyncEvent.JFRUNIT_SYNC_EVENT_NAME);

        stream.onEvent(re -> {
            if (isSyncEvent(re)) {
                watermark.set(re.getLong("sequence"));
                streamStarted.countDown();
            }
            else if (!isInternalSleepEvent(re)) {
                events.add(re);
            }
        });

        stream.startAsync();
        return stream;
    }

    private boolean isSyncEvent(RecordedEvent re) {
        return re.getEventType().getName().equals(SyncEvent.JFRUNIT_SYNC_EVENT_NAME);
    }

    private boolean isInternalSleepEvent(RecordedEvent re) {
        return re.getEventType().getName().equals("jdk.ThreadSleep") &&
                re.getDuration("time").equals(Duration.ofMillis(INTERNAL_WAIT_TIME));
    }

    private List matchEventTypes(List enabledEvents) {
        List allEvents = new ArrayList<>();
        List allEventTypes = FlightRecorder.getFlightRecorder().getEventTypes();

        for (EventConfiguration event : enabledEvents) {
            if (event.name.contains("*")) {
                Pattern pattern = Pattern.compile(event.name.replace("*", ".*"));
                for (EventType eventType : allEventTypes) {
                    if (pattern.matcher(eventType.getName()).matches()) {
                        allEvents.add(new EventConfiguration(eventType.getName(), event.stackTrace, event.threshold, event.period));
                    }
                }
            }
            else {
                allEvents.add(event);
            }
        }

        return allEvents;
    }

    private String getDumpFileName() {
        if (dumpFileName == null) {
            return getDefaultDumpFileName();
        }
        else {
            return dumpFileName.endsWith(".jfr") ? dumpFileName : dumpFileName + ".jfr";
        }
    }

    private String getDefaultDumpFileName() {
        return testMethod.getDeclaringClass().getName() + "-" + testMethod.getName() + ".jfr";
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy