的
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# binjr-adapter-jfr
|
||||
|
||||
[](https://search.maven.org/search?q=g:%22eu.binjr%22%20AND%20a:%22binjr-adapter-jfr%22)
|
||||
|
||||
This module implements a DataAdapter capable of consuming data from JDK Flight Recorder files.
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
dependencies {
|
||||
compileOnly project(':binjr-core')
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes(
|
||||
'Specification-Title': project.name,
|
||||
'Specification-Version': project.version,
|
||||
'Implementation-Title': project.name,
|
||||
'Implementation-Version': project.version,
|
||||
'Build-Number': BINJR_BUILD_NUMBER
|
||||
)
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 eu.binjr.sources.jfr.adapters;
|
||||
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import eu.binjr.common.function.CheckedLambdas;
|
||||
import eu.binjr.common.io.FileSystemBrowser;
|
||||
import eu.binjr.common.io.IOUtils;
|
||||
import eu.binjr.common.javafx.controls.TimeRange;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.core.data.adapters.BaseDataAdapter;
|
||||
import eu.binjr.core.data.adapters.DataAdapter;
|
||||
import eu.binjr.core.data.adapters.ReloadPolicy;
|
||||
import eu.binjr.core.data.exceptions.CannotInitializeDataAdapterException;
|
||||
import eu.binjr.core.data.exceptions.DataAdapterException;
|
||||
import eu.binjr.core.data.exceptions.InvalidAdapterParameterException;
|
||||
import eu.binjr.core.data.indexes.Index;
|
||||
import eu.binjr.core.data.indexes.Indexes;
|
||||
import eu.binjr.core.data.indexes.IndexingStatus;
|
||||
import eu.binjr.core.data.indexes.parser.profile.BuiltInParsingProfile;
|
||||
import eu.binjr.core.data.workspace.TimeSeriesInfo;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.apache.lucene.document.Field;
|
||||
import org.apache.lucene.document.StoredField;
|
||||
import org.apache.lucene.document.StringField;
|
||||
import org.apache.lucene.facet.FacetField;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static eu.binjr.core.data.indexes.parser.capture.CaptureGroup.SEVERITY;
|
||||
|
||||
/**
|
||||
* A {@link DataAdapter} implementation to retrieve data from a JDK Flight Recorder file.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public abstract class BaseJfrDataAdapter<T> extends BaseDataAdapter<T> {
|
||||
private static final Logger logger = Logger.create(BaseJfrDataAdapter.class);
|
||||
protected static final Gson gson = new Gson();
|
||||
protected static final Property<IndexingStatus> INDEXING_OK = new SimpleObjectProperty<>(IndexingStatus.OK);
|
||||
protected static final String ZONE_ID = "zoneId";
|
||||
protected static final String ENCODING = "encoding";
|
||||
protected static final String PARSING_PROFILE = "parsingProfile";
|
||||
protected static final String PATH = "jfrPath";
|
||||
protected JfrEventFormat eventFormat;
|
||||
|
||||
protected Path jfrFilePath;
|
||||
protected ZoneId zoneId;
|
||||
protected String encoding;
|
||||
|
||||
protected Index index;
|
||||
protected FileSystemBrowser fileBrowser;
|
||||
|
||||
|
||||
public BaseJfrDataAdapter(Path jfrPath, ZoneId zoneId) throws DataAdapterException {
|
||||
super();
|
||||
initParams(zoneId, jfrPath, "utf-8");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZoneId getTimeZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getParams() {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put(ZONE_ID, zoneId.toString());
|
||||
params.put(ENCODING, encoding);
|
||||
params.put(PATH, jfrFilePath.toString());
|
||||
return params;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadParams(Map<String, String> params) throws DataAdapterException {
|
||||
if (params == null) {
|
||||
throw new InvalidAdapterParameterException("Could not find parameter list for adapter " + getSourceName());
|
||||
}
|
||||
initParams(validateParameter(params, ZONE_ID,
|
||||
s -> {
|
||||
if (s == null) {
|
||||
throw new InvalidAdapterParameterException("Parameter '" + ZONE_ID + "' is missing in adapter " + getSourceName());
|
||||
}
|
||||
return ZoneId.of(s);
|
||||
}),
|
||||
Paths.get(validateParameterNullity(params, PATH)),
|
||||
validateParameterNullity(params, ENCODING));
|
||||
}
|
||||
|
||||
private void initParams(ZoneId zoneId,
|
||||
Path jfrPath,
|
||||
String encoding) {
|
||||
this.zoneId = zoneId;
|
||||
this.jfrFilePath = jfrPath;
|
||||
this.encoding = encoding;
|
||||
this.eventFormat = new JfrEventFormat(zoneId, Charset.forName(encoding));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() throws DataAdapterException {
|
||||
super.onStart();
|
||||
try {
|
||||
this.fileBrowser = FileSystemBrowser.of(jfrFilePath.getParent());
|
||||
this.index = Indexes.LOG_FILES.acquire();
|
||||
} catch (IOException e) {
|
||||
throw new CannotInitializeDataAdapterException("An error occurred during the data adapter initialization", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
Indexes.LOG_FILES.release();
|
||||
} catch (Exception e) {
|
||||
logger.error("An error occurred while releasing index " + Indexes.LOG_FILES.name() + ": " + e.getMessage());
|
||||
logger.debug("Stack Trace:", e);
|
||||
}
|
||||
IOUtils.close(fileBrowser);
|
||||
super.close();
|
||||
}
|
||||
|
||||
public synchronized void ensureIndexed(Set<String> sources, ReloadPolicy reloadPolicy) throws IOException {
|
||||
if (reloadPolicy == ReloadPolicy.ALL) {
|
||||
sources.forEach(index.getIndexedFiles()::remove);
|
||||
}
|
||||
final LongProperty charRead = new SimpleLongProperty(0);
|
||||
var isCommitNecessary = false;
|
||||
var filterMap = new HashMap<Path, HashSet<String>>();
|
||||
for (var binding : sources) {
|
||||
if (!index.getIndexedFiles().containsKey(binding)) {
|
||||
var a = binding.split("\\|");
|
||||
var filePath = Path.of(a[0].replace(BuiltInParsingProfile.NONE.getProfileId() + "/", ""));
|
||||
var eventType = a[1];
|
||||
filterMap.computeIfAbsent(filePath, p -> new HashSet<>()).add(eventType);
|
||||
isCommitNecessary = true;
|
||||
index.getIndexedFiles().put(binding, IndexingStatus.OK);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<Path, HashSet<String>> entry : filterMap.entrySet()) {
|
||||
Path path = entry.getKey();
|
||||
HashSet<String> strings = entry.getValue();
|
||||
index.add(path.toString(),
|
||||
new JfrRecordingFilter(path, strings),
|
||||
false,
|
||||
eventFormat,
|
||||
(doc, event) -> {
|
||||
// Add number fields
|
||||
event.getNumberFields().forEach((key, value) -> doc.add(new StoredField(key, value.doubleValue())));
|
||||
// Set HAS_NUM field
|
||||
doc.add(new StringField(JfrEventFormat.HAS_NUM_FIELDS, event.getNumberFields().size() > 0 ? "true" : "false", Field.Store.NO));
|
||||
// Add event categories as severity
|
||||
String severity = event.getTextField(JfrEventFormat.CATEGORIES) == null ? "JFR" :
|
||||
event.getTextField(JfrEventFormat.CATEGORIES);//.toLowerCase();
|
||||
doc.add(new FacetField(SEVERITY, severity));
|
||||
doc.add(new StoredField(SEVERITY, severity));
|
||||
return doc;
|
||||
},
|
||||
charRead,
|
||||
INDEXING_OK,
|
||||
(rootPath, parsedEvent) -> BuiltInParsingProfile.NONE.getProfileId() + "/" + rootPath + "|" + parsedEvent.getTextField(JfrEventFormat.EVENT_TYPE_NAME),
|
||||
(source) -> source.eventTypes().stream().map(type -> BuiltInParsingProfile.NONE.getProfileId() + "/" + source.recordingPath().toString() + "|" + type).toList());
|
||||
}
|
||||
if (isCommitNecessary) {
|
||||
index.commitIndexAndTaxonomy();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 eu.binjr.sources.jfr.adapters;
|
||||
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import eu.binjr.common.preferences.ObservablePreference;
|
||||
import eu.binjr.core.data.adapters.DataAdapter;
|
||||
import eu.binjr.core.data.adapters.DataAdapterPreferences;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Defines the preferences associated with the Text files adapter.
|
||||
*/
|
||||
public class JfrAdapterPreferences extends DataAdapterPreferences {
|
||||
private static final Gson gson = new Gson();
|
||||
|
||||
/**
|
||||
* The default text panel font size preference.
|
||||
*/
|
||||
public ObservablePreference<Number> defaultTextViewFontSize = integerPreference("defaultTextViewFontSize", 10);
|
||||
|
||||
/**
|
||||
* The filters used when scanning folders in the source filesystem.
|
||||
*/
|
||||
public ObservablePreference<String[]> folderFilters = objectPreference(String[].class,
|
||||
"folderFilters",
|
||||
new String[]{"*"},
|
||||
gson::toJson,
|
||||
s -> gson.fromJson(s, String[].class));
|
||||
|
||||
|
||||
/**
|
||||
* The filters used to prune file extensions to scan in the source filesystem.
|
||||
*/
|
||||
public ObservablePreference<String[]> fileExtensionFilters = objectPreference(String[].class,
|
||||
"fileExtensionFilters",
|
||||
new String[]{".jfr"},
|
||||
gson::toJson,
|
||||
s -> gson.fromJson(s, String[].class));
|
||||
|
||||
/**
|
||||
* A list of value types for the event payload that should be included
|
||||
*/
|
||||
public ObservablePreference<String[]> includedEventsDataTypes = objectPreference(String[].class,
|
||||
"includedEventsDataTypes",
|
||||
new String[]{"short", "int", "long", "float", "double"},
|
||||
gson::toJson,
|
||||
s -> gson.fromJson(s, String[].class));
|
||||
|
||||
/**
|
||||
* A list of names of events that should be excluded
|
||||
*/
|
||||
public ObservablePreference<String[]> excludedEventsNames = objectPreference(String[].class,
|
||||
"excludedEventsByName",
|
||||
new String[]{"gcId", "javaThreadId", "osThreadId", "modifiers"},
|
||||
gson::toJson,
|
||||
s -> gson.fromJson(s, String[].class));
|
||||
|
||||
/**
|
||||
* Initialize a new instance of the {@link JfrAdapterPreferences} class associated to
|
||||
* a {@link DataAdapter} instance.
|
||||
*
|
||||
* @param dataAdapterClass the associated {@link DataAdapter}
|
||||
*/
|
||||
public JfrAdapterPreferences(Class<? extends DataAdapter<?>> dataAdapterClass) {
|
||||
super(dataAdapterClass);
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 eu.binjr.sources.jfr.adapters;
|
||||
|
||||
|
||||
import eu.binjr.common.javafx.controls.TimeRange;
|
||||
import eu.binjr.common.javafx.controls.TreeViewUtils;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.core.data.adapters.*;
|
||||
import eu.binjr.core.data.exceptions.DataAdapterException;
|
||||
import eu.binjr.core.data.indexes.IndexingStatus;
|
||||
import eu.binjr.core.data.indexes.SearchHit;
|
||||
import eu.binjr.core.data.indexes.parser.profile.BuiltInParsingProfile;
|
||||
import eu.binjr.core.data.timeseries.TimeSeriesProcessor;
|
||||
import eu.binjr.core.data.workspace.TimeSeriesInfo;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import jdk.jfr.EventType;
|
||||
import jdk.jfr.consumer.RecordingFile;
|
||||
import org.eclipse.fx.ui.controls.tree.FilterableTreeItem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A {@link DataAdapter} implementation to retrieve data from a JDK Flight Recorder file.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class JfrDataAdapter extends BaseJfrDataAdapter<SearchHit> implements ProgressAdapter<SearchHit> {
|
||||
private static final Logger logger = Logger.create(JfrDataAdapter.class);
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link JfrDataAdapter} class with a set of default values.
|
||||
*
|
||||
* @throws DataAdapterException if the {@link DataAdapter} could not be initializes.
|
||||
*/
|
||||
public JfrDataAdapter() throws DataAdapterException {
|
||||
super(Path.of(""), ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link JfrDataAdapter} class with the provided parameters.
|
||||
*
|
||||
* @param jfrPath the path to the JFR file.
|
||||
* @param zoneId the time zone to used.
|
||||
* @throws DataAdapterException if the {@link DataAdapter} could not be initialized.
|
||||
*/
|
||||
public JfrDataAdapter(Path jfrPath,
|
||||
ZoneId zoneId)
|
||||
throws DataAdapterException {
|
||||
super(jfrPath, zoneId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeRange getInitialTimeRange(String path, List<TimeSeriesInfo<SearchHit>> seriesInfo) throws DataAdapterException {
|
||||
try {
|
||||
ensureIndexed(seriesInfo.stream().map(info -> BuiltInParsingProfile.NONE.getProfileId() + "/" + info.getBinding().getPath()).collect(Collectors.toSet()), ReloadPolicy.UNLOADED);
|
||||
return index.getTimeRangeBoundaries(seriesInfo.stream().map(ts -> BuiltInParsingProfile.NONE.getProfileId() + "/" + ts.getBinding().getPath()).toList(), getTimeZoneId());
|
||||
} catch (IOException e) {
|
||||
throw new DataAdapterException("Error retrieving initial time range", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterableTreeItem<SourceBinding> getBindingTree() throws DataAdapterException {
|
||||
String rootPath = jfrFilePath.toString() + "|";
|
||||
FilterableTreeItem<SourceBinding> tree = new FilterableTreeItem<>(new LogFilesBinding.Builder()
|
||||
.withLabel(getSourceName())
|
||||
.withPath(jfrFilePath.toString() + "|")
|
||||
.withAdapter(this)
|
||||
.build());
|
||||
try (var recordingFile = new RecordingFile(jfrFilePath)) {
|
||||
for (EventType eventType : recordingFile.readEventTypes()) {
|
||||
var branch = tree;
|
||||
for (var cat : eventType.getCategoryNames()) {
|
||||
var pos = TreeViewUtils.findFirstInTree(tree, item -> item.getValue().getLabel().equals(cat));
|
||||
if (pos.isEmpty()) {
|
||||
var node = new FilterableTreeItem<>((SourceBinding) new LogFilesBinding.Builder()
|
||||
.withLabel(cat)
|
||||
.withParent(branch.getValue())
|
||||
.withPath(branch.getValue().getPath() + "/" + cat)
|
||||
.withAdapter(this)
|
||||
.build());
|
||||
branch.getInternalChildren().add(node);
|
||||
branch = node;
|
||||
} else {
|
||||
branch = (FilterableTreeItem<SourceBinding>) pos.get();
|
||||
}
|
||||
}
|
||||
var leaf = new FilterableTreeItem<>((SourceBinding) new LogFilesBinding.Builder()
|
||||
.withLabel(eventType.getLabel())
|
||||
.withLegend(eventType.getLabel())
|
||||
.withPath(rootPath + eventType.getName())
|
||||
.withParent(branch.getValue())
|
||||
.withAdapter(this)
|
||||
.build());
|
||||
branch.getInternalChildren().add(leaf);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DataAdapterException("Error while attempting to read JFR recording: " + e.getMessage(), e);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public Map<TimeSeriesInfo<SearchHit>, TimeSeriesProcessor<SearchHit>> fetchData(String path,
|
||||
Instant begin,
|
||||
Instant end,
|
||||
List<TimeSeriesInfo<SearchHit>> seriesInfo,
|
||||
boolean bypassCache) throws DataAdapterException {
|
||||
return loadSeries(path, seriesInfo, bypassCache ? ReloadPolicy.ALL : ReloadPolicy.UNLOADED, null, INDEXING_OK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<TimeSeriesInfo<SearchHit>, TimeSeriesProcessor<SearchHit>> loadSeries(String path,
|
||||
List<TimeSeriesInfo<SearchHit>> seriesInfo,
|
||||
ReloadPolicy reloadPolicy,
|
||||
DoubleProperty progress,
|
||||
Property<IndexingStatus> indexingStatus) throws DataAdapterException {
|
||||
Map<TimeSeriesInfo<SearchHit>, TimeSeriesProcessor<SearchHit>> data = new HashMap<>();
|
||||
try {
|
||||
ensureIndexed(seriesInfo.stream().map(info -> BuiltInParsingProfile.NONE.getProfileId() + "/" + info.getBinding().getPath()).collect(Collectors.toSet()), ReloadPolicy.UNLOADED);
|
||||
} catch (Exception e) {
|
||||
throw new DataAdapterException("Error fetching logs from " + path, e);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSourceName() {
|
||||
return new StringBuilder("[JFR: Events] ")
|
||||
.append(jfrFilePath != null ? jfrFilePath.getFileName() : "???")
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 eu.binjr.sources.jfr.adapters;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import eu.binjr.common.javafx.controls.NodeUtils;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.core.data.adapters.DataAdapter;
|
||||
import eu.binjr.core.data.adapters.DataAdapterFactory;
|
||||
import eu.binjr.core.data.exceptions.CannotInitializeDataAdapterException;
|
||||
import eu.binjr.core.data.exceptions.DataAdapterException;
|
||||
import eu.binjr.core.data.exceptions.NoAdapterFoundException;
|
||||
import eu.binjr.core.dialogs.DataAdapterDialog;
|
||||
import eu.binjr.core.dialogs.Dialogs;
|
||||
import eu.binjr.sources.jfr.adapters.charts.JfrChartsDataAdapter;
|
||||
import eu.binjr.sources.jfr.adapters.charts.JfrChartsDataAdapterInfo;
|
||||
import javafx.scene.Node;
|
||||
import javafx.stage.FileChooser;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An implementation of the {@link DataAdapterDialog} class that presents a dialog box to retrieve the parameters specific {@link JfrDataAdapterDialog}
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class JfrDataAdapterDialog extends DataAdapterDialog<Path> {
|
||||
private static final Logger logger = Logger.create(JfrDataAdapterDialog.class);
|
||||
// private final TextField extensionFiltersTextField;
|
||||
private final JfrAdapterPreferences prefs;
|
||||
private static final Gson gson = new Gson();
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link JfrDataAdapterDialog} class.
|
||||
*
|
||||
* @param owner the owner window for the dialog
|
||||
* @throws NoAdapterFoundException if no adapter could be found to get preferences for.
|
||||
*/
|
||||
public JfrDataAdapterDialog(Node owner) throws NoAdapterFoundException {
|
||||
super(owner, Mode.PATH, "mostRecentJfrFiles", false);
|
||||
this.prefs = (JfrAdapterPreferences) DataAdapterFactory.getInstance().getAdapterPreferences(JfrDataAdapter.class.getName());
|
||||
setDialogHeaderText("Add a JFR File");
|
||||
/* extensionFiltersTextField = new TextField(gson.toJson(prefs.fileExtensionFilters.get()));
|
||||
var label = new Label("Extensions:");
|
||||
GridPane.setConstraints(label, 0, 1, 1, 1, HPos.LEFT, VPos.CENTER, Priority.ALWAYS, Priority.ALWAYS, new Insets(4, 0, 4, 0));
|
||||
GridPane.setConstraints(extensionFiltersTextField, 1, 1, 1, 1, HPos.LEFT, VPos.CENTER, Priority.ALWAYS, Priority.ALWAYS, new Insets(4, 0, 4, 0));
|
||||
|
||||
getParamsGridPane().getChildren().addAll(label, extensionFiltersTextField);*/
|
||||
}
|
||||
|
||||
@Override
|
||||
protected File displayFileChooser(Node owner) {
|
||||
try {
|
||||
/* ContextMenu sourceMenu = new ContextMenu();
|
||||
MenuItem fileMenuItem = new MenuItem("JFR file");
|
||||
fileMenuItem.setOnAction(eventHandler -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open JFR File");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("JFR files", "*.jfr"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All files", "*.*", "*"));
|
||||
Dialogs.getInitialDir(getMostRecentList()).ifPresent(fileChooser::setInitialDirectory);
|
||||
File selectedFile = fileChooser.showOpenDialog(NodeUtils.getStage(owner));
|
||||
if (selectedFile != null) {
|
||||
setSourceUri(selectedFile.getPath());
|
||||
}
|
||||
});
|
||||
sourceMenu.getItems().add(fileMenuItem);
|
||||
MenuItem menuItem = new MenuItem("Zip file");
|
||||
menuItem.setOnAction(eventHandler -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Zip Archive");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Zip archive", "*.zip"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All files", "*.*", "*"));
|
||||
Dialogs.getInitialDir(getMostRecentList()).ifPresent(fileChooser::setInitialDirectory);
|
||||
File selectedFile = fileChooser.showOpenDialog(NodeUtils.getStage(owner));
|
||||
if (selectedFile != null) {
|
||||
setSourceUri(selectedFile.getPath());
|
||||
}
|
||||
});
|
||||
sourceMenu.getItems().add(menuItem);
|
||||
MenuItem folderMenuItem = new MenuItem("Folder");
|
||||
folderMenuItem.setOnAction(eventHandler -> {
|
||||
DirectoryChooser dirChooser = new DirectoryChooser();
|
||||
dirChooser.setTitle("Open Folder");
|
||||
Dialogs.getInitialDir(getMostRecentList()).ifPresent(dirChooser::setInitialDirectory);
|
||||
File selectedFile = dirChooser.showDialog(NodeUtils.getStage(owner));
|
||||
if (selectedFile != null) {
|
||||
setSourceUri(selectedFile.getPath());
|
||||
}
|
||||
});
|
||||
sourceMenu.getItems().add(folderMenuItem);
|
||||
sourceMenu.show(owner, Side.RIGHT, 0, 0);*/
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open JFR File");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("JFR files", "*.jfr"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All files", "*.*", "*"));
|
||||
Dialogs.getInitialDir(getMostRecentList()).ifPresent(fileChooser::setInitialDirectory);
|
||||
File selectedFile = fileChooser.showOpenDialog(NodeUtils.getStage(owner));
|
||||
if (selectedFile != null) {
|
||||
return selectedFile;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Dialogs.notifyException("Error while displaying file chooser: " + e.getMessage(), e, owner);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<DataAdapter> getDataAdapters() throws DataAdapterException {
|
||||
Path path = Paths.get(getSourceUri());
|
||||
if (!Files.exists(path)) {
|
||||
throw new CannotInitializeDataAdapterException("Cannot find " + getSourceUri());
|
||||
}
|
||||
if (!path.isAbsolute()) {
|
||||
throw new CannotInitializeDataAdapterException("The provided path is not valid.");
|
||||
}
|
||||
getMostRecentList().push(path);
|
||||
// prefs.fileExtensionFilters.set(gson.fromJson(extensionFiltersTextField.getText(), String[].class));
|
||||
return List.of(new JfrDataAdapter(path, ZoneId.of(getSourceTimezone())),
|
||||
new JfrChartsDataAdapter(path, ZoneId.of(getSourceTimezone())));
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 eu.binjr.sources.jfr.adapters;
|
||||
|
||||
|
||||
import eu.binjr.core.data.adapters.AdapterMetadata;
|
||||
import eu.binjr.core.data.adapters.BaseDataAdapterInfo;
|
||||
import eu.binjr.core.data.adapters.SourceLocality;
|
||||
import eu.binjr.core.data.adapters.VisualizationType;
|
||||
import eu.binjr.core.data.exceptions.CannotInitializeDataAdapterException;
|
||||
import eu.binjr.core.preferences.AppEnvironment;
|
||||
|
||||
|
||||
/**
|
||||
* Defines the metadata associated with JfrDataAdapter.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@AdapterMetadata(
|
||||
name = "JFR",
|
||||
description = "JDK Flight Recorder Files Data Adapter (Events view)",
|
||||
copyright = AppEnvironment.COPYRIGHT_NOTICE,
|
||||
license = AppEnvironment.LICENSE,
|
||||
siteUrl = AppEnvironment.HTTP_WWW_BINJR_EU,
|
||||
adapterClass = JfrDataAdapter.class,
|
||||
dialogClass = JfrDataAdapterDialog.class,
|
||||
preferencesClass = JfrAdapterPreferences.class,
|
||||
sourceLocality = SourceLocality.LOCAL,
|
||||
apiLevel = AppEnvironment.PLUGIN_API_LEVEL,
|
||||
visualizationType = VisualizationType.EVENTS
|
||||
)
|
||||
public class JfrDataAdapterInfo extends BaseDataAdapterInfo {
|
||||
|
||||
/**
|
||||
* Initialises a new instance of the {@link JfrDataAdapterInfo} class.
|
||||
*
|
||||
* @throws CannotInitializeDataAdapterException if the adapter's initialization failed
|
||||
*/
|
||||
public JfrDataAdapterInfo() throws CannotInitializeDataAdapterException {
|
||||
super(JfrDataAdapterInfo.class);
|
||||
}
|
||||
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 eu.binjr.sources.jfr.adapters;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.core.data.adapters.DataAdapterFactory;
|
||||
import eu.binjr.core.data.exceptions.NoAdapterFoundException;
|
||||
import eu.binjr.core.data.indexes.parser.EventFormat;
|
||||
import eu.binjr.core.data.indexes.parser.EventParser;
|
||||
import eu.binjr.core.data.indexes.parser.profile.BuiltInParsingProfile;
|
||||
import eu.binjr.core.data.indexes.parser.profile.ParsingProfile;
|
||||
import jdk.jfr.MemoryAddress;
|
||||
import jdk.jfr.Timestamp;
|
||||
import jdk.jfr.ValueDescriptor;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public class JfrEventFormat implements EventFormat<JfrRecordingFilter> {
|
||||
public static final String EVENT_TYPE_NAME = "eventTypeName";
|
||||
public static final String JDK_CPULOAD = "jdk.CPULoad";
|
||||
public static final String JVM_SYSTEM = "jvmSystem";
|
||||
public static final String JVM_USER = "jvmUser";
|
||||
public static final String MACHINE_TOTAL = "machineTotal";
|
||||
private static final Logger logger = Logger.create(JfrEventParser.class);
|
||||
public static final String CATEGORIES = "categories";
|
||||
public static final String HAS_NUM_FIELDS = "hasNumFields";
|
||||
public static final String GCREF_TYPE_FIELD = "type";
|
||||
public static final String JDK_GCREFERENCE_STATISTICS = "jdk.GCReferenceStatistics";
|
||||
public static final String GCREF_COUNT_FIELD = "count";
|
||||
public static final String JDK_TYPES_THREAD_GROUP = "jdk.types.ThreadGroup";
|
||||
public static final String GCREF_FINAL_REFERENCE = "Final reference";
|
||||
public static final String GCREF_SOFT_REFERENCE = "Soft reference";
|
||||
public static final String GCREF_WEAK_REFERENCE = "Weak reference";
|
||||
public static final String GCREF_PHANTOM_REFERENCE = "Phantom reference";
|
||||
public static final String GCREF_TOTAL_COUNT = "Total Count";
|
||||
public static final String JDK_TYPES_STACK_TRACE = "jdk.types.StackTrace";
|
||||
public static final String JDK_GARBAGE_COLLECTION = "jdk.GarbageCollection";
|
||||
private final ZoneId zoneId;
|
||||
private final Charset encoding;
|
||||
private static final JfrAdapterPreferences adapterPrefs;
|
||||
|
||||
static {
|
||||
try {
|
||||
adapterPrefs = (JfrAdapterPreferences) DataAdapterFactory.getInstance().getAdapterPreferences(JfrDataAdapter.class.getName());
|
||||
} catch (NoAdapterFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public JfrEventFormat(ZoneId zoneId, Charset encoding) {
|
||||
|
||||
this.zoneId = zoneId;
|
||||
this.encoding = encoding;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParsingProfile getProfile() {
|
||||
return BuiltInParsingProfile.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventParser parse(JfrRecordingFilter source) {
|
||||
return new JfrEventParser(this, source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Charset getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZoneId getZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
public static boolean includeField(ValueDescriptor field) {
|
||||
return field.getAnnotation(Timestamp.class) == null &&
|
||||
field.getAnnotation(MemoryAddress.class) ==null &&
|
||||
Arrays.stream(adapterPrefs.includedEventsDataTypes.get()).anyMatch(s -> Objects.equals(s, field.getTypeName())) &&
|
||||
Arrays.stream(adapterPrefs.excludedEventsNames.get()).noneMatch(s -> Objects.equals(s, field.getName()));
|
||||
}
|
||||
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 eu.binjr.sources.jfr.adapters;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.common.logging.Profiler;
|
||||
import eu.binjr.core.data.indexes.parser.EventParser;
|
||||
import eu.binjr.core.data.indexes.parser.ParsedEvent;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import jdk.jfr.consumer.RecordedEvent;
|
||||
import jdk.jfr.consumer.RecordedObject;
|
||||
import jdk.jfr.consumer.RecordingFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
|
||||
public class JfrEventParser implements EventParser {
|
||||
private static final Logger logger = Logger.create(JfrEventParser.class);
|
||||
|
||||
|
||||
// private final BufferedReader reader;
|
||||
private final AtomicLong sequence;
|
||||
private final JfrEventFormat format;
|
||||
private final JfrEventIterator eventIterator;
|
||||
private final RecordingFile recordingFile;
|
||||
private final LongProperty progress = new SimpleLongProperty(0);
|
||||
private final JfrRecordingFilter eventTypeFilter;
|
||||
|
||||
JfrEventParser(JfrEventFormat format, JfrRecordingFilter filter) {
|
||||
this.sequence = new AtomicLong(0);
|
||||
this.format = format;
|
||||
try (var p = Profiler.start("Initialize JFR Recording file", logger::perf)) {
|
||||
this.eventTypeFilter = filter;
|
||||
this.recordingFile = new RecordingFile(filter.recordingPath());
|
||||
this.eventIterator = new JfrEventIterator();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
recordingFile.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongProperty progressIndicator() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<ParsedEvent> iterator() {
|
||||
return eventIterator;
|
||||
}
|
||||
|
||||
public class JfrEventIterator implements Iterator<ParsedEvent> {
|
||||
|
||||
@Override
|
||||
public ParsedEvent next() {
|
||||
ParsedEvent event = null;
|
||||
while (event == null && recordingFile.hasMoreEvents()) {
|
||||
event = readNextJrfEvent();
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
private ParsedEvent readNextJrfEvent() {
|
||||
RecordedEvent jfrEvent = null;
|
||||
try {
|
||||
jfrEvent = recordingFile.readEvent();
|
||||
if (!eventTypeFilter.eventTypes().contains(jfrEvent.getEventType().getName())) {
|
||||
return null;
|
||||
}
|
||||
ZonedDateTime timestamp = ZonedDateTime.ofInstant(jfrEvent.getStartTime(), format.getZoneId());
|
||||
Map<String, Number> numFields = new LinkedHashMap<>();
|
||||
var categories = jfrEvent.getEventType().getCategoryNames();
|
||||
String catFacet = categories.size() > 1 ? categories.get(1) : categories.get(0);
|
||||
switch (jfrEvent.getEventType().getName()) {
|
||||
case JfrEventFormat.JDK_GCREFERENCE_STATISTICS ->
|
||||
numFields.put(String.join(" ", jfrEvent.getValue(JfrEventFormat.GCREF_TYPE_FIELD),
|
||||
JfrEventFormat.GCREF_TOTAL_COUNT), jfrEvent.getValue(JfrEventFormat.GCREF_COUNT_FIELD));
|
||||
default -> addField("", jfrEvent, numFields);
|
||||
}
|
||||
return new ParsedEvent(
|
||||
sequence.incrementAndGet(),
|
||||
timestamp,
|
||||
jfrEvent.toString(),
|
||||
Map.of(JfrEventFormat.CATEGORIES, catFacet, JfrEventFormat.EVENT_TYPE_NAME, jfrEvent.getEventType().getName()),
|
||||
numFields);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error parsing JFR event [" + (jfrEvent == null ? "unknown" : jfrEvent.getEventType().getName()) + "]: " + e.getMessage());
|
||||
logger.debug("Stack trace", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void addField(String parentLabel, RecordedObject jfrEvent, Map<String, Number> numFields) {
|
||||
for (var field : jfrEvent.getFields()) {
|
||||
if (JfrEventFormat.includeField(field)) {
|
||||
numFields.put(String.join(" ", parentLabel, field.getLabel()).trim(), jfrEvent.getValue(field.getName()));
|
||||
}
|
||||
if (!field.getFields().isEmpty() && jfrEvent.getValue(field.getName()) instanceof RecordedObject nestedEvent) {
|
||||
addField(field.getLabel(), nestedEvent, numFields);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return recordingFile.hasMoreEvents();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 eu.binjr.sources.jfr.adapters;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
public record JfrRecordingFilter(Path recordingPath, Set<String> eventTypes) {
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 eu.binjr.sources.jfr.adapters.charts;
|
||||
|
||||
|
||||
import eu.binjr.common.javafx.controls.TimeRange;
|
||||
import eu.binjr.common.javafx.controls.TreeViewUtils;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.core.data.adapters.DataAdapter;
|
||||
import eu.binjr.core.data.adapters.ReloadPolicy;
|
||||
import eu.binjr.core.data.adapters.SourceBinding;
|
||||
import eu.binjr.core.data.adapters.TimeSeriesBinding;
|
||||
import eu.binjr.core.data.exceptions.DataAdapterException;
|
||||
import eu.binjr.core.data.indexes.parser.profile.BuiltInParsingProfile;
|
||||
import eu.binjr.core.data.timeseries.DoubleTimeSeriesProcessor;
|
||||
import eu.binjr.core.data.timeseries.TimeSeriesProcessor;
|
||||
import eu.binjr.core.data.workspace.ChartType;
|
||||
import eu.binjr.core.data.workspace.TimeSeriesInfo;
|
||||
import eu.binjr.core.data.workspace.UnitPrefixes;
|
||||
import eu.binjr.sources.jfr.adapters.BaseJfrDataAdapter;
|
||||
import eu.binjr.sources.jfr.adapters.JfrEventFormat;
|
||||
import javafx.scene.paint.Color;
|
||||
import jdk.jfr.*;
|
||||
import jdk.jfr.consumer.RecordingFile;
|
||||
import org.eclipse.fx.ui.controls.tree.FilterableTreeItem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A {@link DataAdapter} implementation to retrieve data from a JDK Flight Recorder file.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class JfrChartsDataAdapter extends BaseJfrDataAdapter<Double> {
|
||||
private static final Logger logger = Logger.create(JfrChartsDataAdapter.class);
|
||||
public static final int RECURSE_MAX_DEPTH = 10;
|
||||
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link JfrChartsDataAdapter} class with a set of default values.
|
||||
*
|
||||
* @throws DataAdapterException if the {@link DataAdapter} could not be initializes.
|
||||
*/
|
||||
public JfrChartsDataAdapter() throws DataAdapterException {
|
||||
super(Path.of(""), ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link JfrChartsDataAdapter} class with the provided parameters.
|
||||
*
|
||||
* @param jfrPath the path to the JFR file.
|
||||
* @param zoneId the time zone to used.
|
||||
* @throws DataAdapterException if the {@link DataAdapter} could not be initialized.
|
||||
*/
|
||||
public JfrChartsDataAdapter(Path jfrPath,
|
||||
ZoneId zoneId)
|
||||
throws DataAdapterException {
|
||||
super(jfrPath, zoneId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeRange getInitialTimeRange(String path, List<TimeSeriesInfo<Double>> seriesInfo) throws DataAdapterException {
|
||||
try {
|
||||
ensureIndexed(seriesInfo.stream().map(info -> info.getBinding().getPath()).collect(Collectors.toSet()), ReloadPolicy.UNLOADED);
|
||||
return index.getTimeRangeBoundaries(seriesInfo.stream().map(ts -> ts.getBinding().getPath()).toList(), getTimeZoneId());
|
||||
} catch (IOException e) {
|
||||
throw new DataAdapterException("Error retrieving initial time range", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterableTreeItem<SourceBinding> getBindingTree() throws DataAdapterException {
|
||||
String rootPath = BuiltInParsingProfile.NONE.getProfileId() + "/" + jfrFilePath.toString() + "|";
|
||||
FilterableTreeItem<SourceBinding> tree = new FilterableTreeItem<>(new TimeSeriesBinding.Builder()
|
||||
.withLabel(getSourceName())
|
||||
.withPath(rootPath)
|
||||
.withAdapter(this)
|
||||
.build());
|
||||
try (var recordingFile = new RecordingFile(jfrFilePath)) {
|
||||
for (EventType eventType : recordingFile.readEventTypes()) {
|
||||
var branch = tree;
|
||||
for (var cat : eventType.getCategoryNames()) {
|
||||
var pos = TreeViewUtils.findFirstInTree(tree, item -> item.getValue().getLabel().equals(cat));
|
||||
if (pos.isEmpty()) {
|
||||
var node = new FilterableTreeItem<>((SourceBinding) new TimeSeriesBinding.Builder()
|
||||
.withLabel(cat)
|
||||
.withParent(branch.getValue())
|
||||
.withPath(branch.getValue().getPath() + "/" + cat)
|
||||
.withAdapter(this)
|
||||
.build());
|
||||
branch.getInternalChildren().add(node);
|
||||
branch = node;
|
||||
} else {
|
||||
branch = (FilterableTreeItem<SourceBinding>) pos.get();
|
||||
}
|
||||
}
|
||||
var leaf = new FilterableTreeItem<>((SourceBinding) new TimeSeriesBinding.Builder()
|
||||
.withLabel(eventType.getLabel())
|
||||
.withLegend(eventType.getLabel())
|
||||
.withPath(rootPath + eventType.getName())
|
||||
.withParent(branch.getValue())
|
||||
.withGraphType(ChartType.LINE)
|
||||
.withAdapter(this)
|
||||
.build());
|
||||
branch.getInternalChildren().add(leaf);
|
||||
switch (eventType.getName()) {
|
||||
case JfrEventFormat.JDK_GCREFERENCE_STATISTICS -> {
|
||||
addField(JfrEventFormat.GCREF_FINAL_REFERENCE, eventType.getField(JfrEventFormat.GCREF_COUNT_FIELD), leaf, 1);
|
||||
addField(JfrEventFormat.GCREF_SOFT_REFERENCE, eventType.getField(JfrEventFormat.GCREF_COUNT_FIELD), leaf, 1);
|
||||
addField(JfrEventFormat.GCREF_WEAK_REFERENCE, eventType.getField(JfrEventFormat.GCREF_COUNT_FIELD), leaf, 1);
|
||||
addField(JfrEventFormat.GCREF_PHANTOM_REFERENCE, eventType.getField(JfrEventFormat.GCREF_COUNT_FIELD), leaf, 1);
|
||||
}
|
||||
case JfrEventFormat.JDK_CPULOAD ->{
|
||||
var jvmBranch =new FilterableTreeItem<>((SourceBinding) new TimeSeriesBinding.Builder()
|
||||
.withLabel("JVM")
|
||||
.withPath(leaf.getValue().getPath())
|
||||
.withParent(leaf.getValue())
|
||||
.withUnitName("%")
|
||||
.withPrefix(UnitPrefixes.PERCENTAGE)
|
||||
.withGraphType(ChartType.STACKED)
|
||||
.withAdapter(this)
|
||||
.build());
|
||||
leaf.getInternalChildren().add(jvmBranch);
|
||||
|
||||
jvmBranch.getInternalChildren().add(new FilterableTreeItem<>(new TimeSeriesBinding.Builder()
|
||||
.withLabel(eventType.getField(JfrEventFormat.JVM_SYSTEM).getLabel())
|
||||
.withPath(jvmBranch.getValue().getPath())
|
||||
.withParent(jvmBranch.getValue())
|
||||
.withUnitName("%")
|
||||
.withPrefix(UnitPrefixes.PERCENTAGE)
|
||||
.withColor(Color.RED)
|
||||
.withGraphType(ChartType.LINE)
|
||||
.withAdapter(this)
|
||||
.build()));
|
||||
|
||||
jvmBranch.getInternalChildren().add(new FilterableTreeItem<>(new TimeSeriesBinding.Builder()
|
||||
.withLabel(eventType.getField(JfrEventFormat.JVM_USER).getLabel())
|
||||
.withPath(jvmBranch.getValue().getPath())
|
||||
.withParent(jvmBranch.getValue())
|
||||
.withUnitName("%")
|
||||
.withPrefix(UnitPrefixes.PERCENTAGE)
|
||||
.withColor(Color.DARKGREEN)
|
||||
.withGraphType(ChartType.LINE)
|
||||
.withAdapter(this)
|
||||
.build()));
|
||||
|
||||
var machineBranch =new FilterableTreeItem<>((SourceBinding) new TimeSeriesBinding.Builder()
|
||||
.withLabel("Machine")
|
||||
.withPath(leaf.getValue().getPath())
|
||||
.withParent(leaf.getValue())
|
||||
.withUnitName("%")
|
||||
.withPrefix(UnitPrefixes.PERCENTAGE)
|
||||
.withGraphType(ChartType.LINE)
|
||||
.withAdapter(this)
|
||||
.build());
|
||||
leaf.getInternalChildren().add(machineBranch);
|
||||
machineBranch.getInternalChildren().add(new FilterableTreeItem<>(new TimeSeriesBinding.Builder()
|
||||
.withLabel(eventType.getField(JfrEventFormat.MACHINE_TOTAL).getLabel())
|
||||
.withPath(machineBranch.getValue().getPath())
|
||||
.withParent(machineBranch.getValue())
|
||||
.withUnitName("%")
|
||||
.withPrefix(UnitPrefixes.PERCENTAGE)
|
||||
.withGraphType(ChartType.LINE)
|
||||
.withAdapter(this)
|
||||
.build()));
|
||||
}
|
||||
default -> {
|
||||
for (var field : eventType.getFields()) {
|
||||
addField("", field, leaf, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new DataAdapterException("Error while attempting to read JFR recording: " + e.getMessage(), e);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
private void addField(String parentName, ValueDescriptor field, FilterableTreeItem<SourceBinding> leaf, int depth) {
|
||||
if (JfrEventFormat.includeField(field)) {
|
||||
var unit = extractKnownUnit(field);
|
||||
leaf.getInternalChildren().add(new FilterableTreeItem<>(new TimeSeriesBinding.Builder()
|
||||
.withLabel(String.join(" ", parentName, field.getLabel()).trim())
|
||||
.withPath(leaf.getValue().getPath())
|
||||
.withParent(leaf.getValue())
|
||||
.withUnitName(unit.name())
|
||||
.withPrefix(unit.prefix())
|
||||
.withGraphType(ChartType.LINE)
|
||||
.withAdapter(this)
|
||||
.build()));
|
||||
}
|
||||
for (var nestedField : field.getFields()) {
|
||||
if (!field.getTypeName().equals(JfrEventFormat.JDK_TYPES_THREAD_GROUP) &&
|
||||
!field.getTypeName().equals(JfrEventFormat.JDK_TYPES_STACK_TRACE) &&
|
||||
depth <= RECURSE_MAX_DEPTH) {
|
||||
addField(field.getLabel(), nestedField, leaf, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<TimeSeriesInfo<Double>, TimeSeriesProcessor<Double>> fetchData(String path,
|
||||
Instant begin,
|
||||
Instant end,
|
||||
List<TimeSeriesInfo<Double>> seriesInfo,
|
||||
boolean bypassCache) throws DataAdapterException {
|
||||
try {
|
||||
ensureIndexed(seriesInfo.stream().map(info -> info.getBinding().getPath()).collect(Collectors.toSet()), ReloadPolicy.UNLOADED);
|
||||
Map<TimeSeriesInfo<Double>, TimeSeriesProcessor<Double>> series = new HashMap<>();
|
||||
for (TimeSeriesInfo<Double> info : seriesInfo) {
|
||||
series.put(info, new DoubleTimeSeriesProcessor());
|
||||
}
|
||||
var nbHits = index.search(
|
||||
begin.toEpochMilli(),
|
||||
end.toEpochMilli(),
|
||||
series,
|
||||
getTimeZoneId(),
|
||||
bypassCache);
|
||||
logger.debug(() -> "Retrieved " + nbHits + " hits");
|
||||
return series;
|
||||
} catch (Exception e) {
|
||||
throw new DataAdapterException("Error fetching data from " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getSourceName() {
|
||||
return new StringBuilder("[JFR: Charts] ")
|
||||
.append(jfrFilePath != null ? jfrFilePath.getFileName() : "???")
|
||||
.toString();
|
||||
}
|
||||
|
||||
private Unit extractKnownUnit(ValueDescriptor field) {
|
||||
var timespan = field.getAnnotation(Timespan.class);
|
||||
if (timespan != null) {
|
||||
return new Unit(timespan.value(), UnitPrefixes.METRIC);
|
||||
}
|
||||
|
||||
var frequency = field.getAnnotation(Frequency.class);
|
||||
if (frequency != null) {
|
||||
return new Unit("Hertz", UnitPrefixes.METRIC);
|
||||
}
|
||||
|
||||
var dataAmount = field.getAnnotation(DataAmount.class);
|
||||
if (dataAmount != null) {
|
||||
return new Unit(dataAmount.value(), UnitPrefixes.BINARY);
|
||||
}
|
||||
|
||||
var percentage = field.getAnnotation(Percentage.class);
|
||||
if (percentage != null) {
|
||||
return new Unit("%", UnitPrefixes.NONE);
|
||||
}
|
||||
|
||||
return new Unit("-", UnitPrefixes.NONE);
|
||||
}
|
||||
|
||||
private record Unit(String name, UnitPrefixes prefix) {
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 eu.binjr.sources.jfr.adapters.charts;
|
||||
|
||||
|
||||
import eu.binjr.core.data.adapters.AdapterMetadata;
|
||||
import eu.binjr.core.data.adapters.BaseDataAdapterInfo;
|
||||
import eu.binjr.core.data.adapters.SourceLocality;
|
||||
import eu.binjr.core.data.adapters.VisualizationType;
|
||||
import eu.binjr.core.data.exceptions.CannotInitializeDataAdapterException;
|
||||
import eu.binjr.core.preferences.AppEnvironment;
|
||||
import eu.binjr.sources.jfr.adapters.JfrDataAdapterDialog;
|
||||
|
||||
|
||||
/**
|
||||
* Defines the metadata associated with JfrDataAdapter.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@AdapterMetadata(
|
||||
name = "JFR - Charts)",
|
||||
description = "JDK Flight Recorder Charts Data Adapter",
|
||||
copyright = AppEnvironment.COPYRIGHT_NOTICE,
|
||||
license = AppEnvironment.LICENSE,
|
||||
siteUrl = AppEnvironment.HTTP_WWW_BINJR_EU,
|
||||
adapterClass = JfrChartsDataAdapter.class,
|
||||
dialogClass = JfrDataAdapterDialog.class,
|
||||
sourceLocality = SourceLocality.LOCAL,
|
||||
apiLevel = AppEnvironment.PLUGIN_API_LEVEL,
|
||||
visualizationType = VisualizationType.CHARTS
|
||||
)
|
||||
public class JfrChartsDataAdapterInfo extends BaseDataAdapterInfo {
|
||||
|
||||
/**
|
||||
* Initialises a new instance of the {@link JfrChartsDataAdapterInfo} class.
|
||||
*
|
||||
* @throws CannotInitializeDataAdapterException if the adapter's initialization failed
|
||||
*/
|
||||
public JfrChartsDataAdapterInfo() throws CannotInitializeDataAdapterException {
|
||||
super(JfrChartsDataAdapterInfo.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
#
|
||||
# Copyright 2023 Frederic Thevenet
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# http://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.
|
||||
#
|
||||
|
||||
# JFR Data adapter service implementation
|
||||
eu.binjr.sources.jfr.adapters.JfrDataAdapterInfo
|
||||
eu.binjr.sources.jfr.adapters.charts.JfrChartsDataAdapterInfo
|
||||
|
||||
Reference in New Issue
Block a user