package com.insightfullogic.honest_profiler.ports.javafx.model; import static javafx.application.Platform.isFxApplicationThread; import static javafx.application.Platform.runLater; import static javafx.util.Duration.seconds; import java.io.File; import java.util.concurrent.atomic.AtomicInteger; import com.insightfullogic.honest_profiler.core.aggregation.AggregationProfile; import com.insightfullogic.honest_profiler.core.collector.lean.ProfileSource; import com.insightfullogic.honest_profiler.core.profiles.FlameGraph; import com.insightfullogic.honest_profiler.core.profiles.FlameGraphListener; import com.insightfullogic.honest_profiler.core.profiles.lean.LeanProfile; import com.insightfullogic.honest_profiler.core.profiles.lean.LeanProfileListener; import com.insightfullogic.honest_profiler.ports.javafx.model.task.AggregateProfileTask; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.util.Duration; /** * A ProfileContext contains state which needs to be shared by all controllers created for the same profile, and methods * to access and change that state. */ public class ProfileContext { // Class Properties /** * Enumeration listing the different profiling modes. */ public static enum ProfileMode { /** Live profile, monitoring a running JVM. */ LIVE, /** Log profile, read from a Log File produced by a finished Profiler Agent session. */ LOG } private static final AtomicInteger counter = new AtomicInteger(); // Instance Properties private final ApplicationContext appCtx; private final int id; private final SimpleStringProperty name; private final File file; private final ProfileMode mode; private ProfileSource profileSource; private final SimpleObjectProperty<AggregationProfile> profile; private final SimpleObjectProperty<FlameGraph> flameGraph; private boolean frozen; private Duration refreshInterval; private Timeline timeline; // While frozen, incoming profiles/graphs are cached in the following 2 instance properties. private LeanProfile cachedProfile; private FlameGraph cachedFlameGraph; // Instance Constructors /** * Constructor which specifies the {@link ApplicationContext}, the name of the context, its * {@link ProfileContext.ProfileMode} and the Log File containing the information emitted by the Profiler Agent. * <p> * @param appCtx the {@link ApplicationContext} for the application * @param name the name of the ProfileContext * @param mode the {@link ProfileContext.ProfileMode} * @param file the Log File containing the information emitted by the Profiler Agent */ public ProfileContext(ApplicationContext appCtx, String name, ProfileMode mode, File file) { this.appCtx = appCtx; this.name = new SimpleStringProperty(name); this.mode = mode; this.file = file; profileSource = null; // Store a unique id for the ProfileContext id = counter.incrementAndGet(); profile = new SimpleObjectProperty<>(); flameGraph = new SimpleObjectProperty<>(); refreshInterval = seconds(1); } // Instance Accessors /** * Returns the {@link File} containing the information emitted by the Profiler Agent. * <p> * @return the {@link File} containing the information emitted by the Profiler Agent */ public File getFile() { return file; } /** * Sets the {@link ProfileSource} which generates new {@link LeanProfile}s, and starts the polling mechanism. * <p> * @param profileSource the {@link ProfileSource} which generates new {@link LeanProfile}s */ public void setProfileSource(ProfileSource profileSource) { this.profileSource = profileSource; newTimeline(); } /** * Returns the interval, in seconds, at which the ProfileContext will request new {@link LeanProfile} instances from * the {@link ProfileSource}. * <p> * @return the interval, in seconds, at which the ProfileContext will request new {@link LeanProfile} instances */ public int getDuration() { return (int)refreshInterval.toSeconds(); } /** * Sets the interval, in seconds, at which the ProfileContext will request new {@link LeanProfile} instances from * the {@link ProfileSource}. * <p> * @param seconds the interval, in seconds, at which the ProfileContext will request new {@link LeanProfile} * instances */ public void setDuration(int seconds) { refreshInterval = seconds(seconds); updateTimeline(); } /** * Returns the unique id of this ProfileContext. * <p> * @return the unique id of this ProfileContext */ public int getId() { return id; } /** * Returns the name of this ProfileContext. * <p> * @return the name of this ProfileContext */ public String getName() { return name.get(); } /** * Returns the {@link ProfileMode} for this ProfileContext. * <p> * @return the {@link ProfileMode} for this ProfileContext */ public ProfileMode getMode() { return mode; } /** * Returns the current {@link AggregationProfile}. * <p> * @return the current {@link AggregationProfile} */ public AggregationProfile getProfile() { return profile.get(); } /** * Returns the {@link ObjectProperty} encapsulating the current {@link AggregationProfile}. * <p> * @return the {@link ObjectProperty} encapsulating the current {@link AggregationProfile} */ public ObjectProperty<AggregationProfile> profileProperty() { return profile; } /** * Returns the {@link ObjectProperty} encapsulating the current {@link FlameGraph}. * <p> * @return the {@link ObjectProperty} encapsulating the current {@link FlameGraph} */ public ObjectProperty<FlameGraph> flameGraphProperty() { return flameGraph; } /** * Returns a boolean indicating whether the ProfileContext is currently frozen, i.e. not requesting any * {@link LeanProfile} updates from the {@link ProfileSource}. * <p> * @return a boolean indicating whether the ProfileContext is currently frozen */ public boolean isFrozen() { return frozen; } /** * Freezes or unfreezes the {@link ProfileContext}. This method may only be called on the FX thread. * <p> * @param freeze a boolean indicating whether the ProfileContext should be frozen */ public void setFrozen(boolean freeze) { frozen = freeze; if (freeze) { // The timeline is the timing mechanism which will request LeanProfiles at a rate specified by the refresh // interval. timeline.pause(); } else { // The timeline is the timing mechanism which will request LeanProfiles at a rate specified by the refresh // interval. timeline.play(); // If any unrequested LeanProfiles were emitted by the ProfileSource while frozen (which should only happen // if the Profiler Agent has finished, which will trigger an end-of-log event), these are cached in the // cachedProfile property. When unfreezing, such cached profiles are processed here, to ensure the most // recent emitted LeanProfile is definitely shown. appCtx.execute(new AggregateProfileTask(ProfileContext.this, cachedProfile)); if (cachedFlameGraph != null) { update(cachedFlameGraph); cachedFlameGraph = null; } } } /** * Returns a {@link LeanProfileListener} which accepts new emitted {@link LeanProfile}s and updates the * ProfileContext accordingly. * <p> * @return a {@link LeanProfileListener} which accepts new emitted {@link LeanProfile}s */ public LeanProfileListener getProfileListener() { return new LeanProfileListener() { @Override public void accept(LeanProfile profile) { // Don't do anything in trivial situations. if (profile == null || profile == cachedProfile) { return; } cachedProfile = profile; if (!frozen) { appCtx.execute(new AggregateProfileTask(ProfileContext.this, profile)); } } }; } /** * Returns a {@link FlameGraphListener} which accepts new emitted {@link FlameGraph}s and updates the ProfileContext * accordingly. * <p> * @return a {@link FlameGraphListener} which accepts new emitted {@link FlameGraph}s */ public FlameGraphListener getFlameGraphListener() { return new FlameGraphListener() { @Override public void accept(FlameGraph t) { // Ensure the update is run on the FX Thread. if (isFxApplicationThread()) { update(t); } else { runLater(() -> update(t)); } } }; } /** * Update the {@link AggregationProfile} {@link ObjectProperty}. This method may only be called on the FX thread. * <p> * @param profile the new {@link AggregationProfile} */ public void update(AggregationProfile profile) { this.profile.set(profile); } /** * Update the {@link FlameGraph} {@link ObjectProperty} if the ProfileContext is not frozen, or cache it if frozen. * This method may only be called on the FX thread. * <p> * @param flameGraph the new {@link FlameGraph} */ private void update(FlameGraph flameGraph) { if (frozen) { cachedFlameGraph = flameGraph; } else { this.flameGraph.set(flameGraph); } } /** * Stops the current {@link Timeline}, and ensures that the moment it stops, it starts a new {@link Timeline} which * will pick up the currently set refresh rate. */ private void updateTimeline() { // Since a TimeLine is not guaranteed to stop immediately, we use this mechanism that a new TimeLine is started // only when the Timeline has definitely stopped, to avoid any potential concurrency issues. timeline.setOnFinished(event -> newTimeline()); // Aaaaand... Cut ! timeline.stop(); } /** * Start a new {@link Timeline} which will request {@link LeanProfile} updates from the {@link ProfileSource} at the * rate specified by the refresh interval. */ private void newTimeline() { timeline = new Timeline( new KeyFrame(refreshInterval, e -> profileSource.requestProfile())); timeline.setCycleCount(Timeline.INDEFINITE); timeline.play(); } }