diff --git a/README.md b/README.md index a20b493c..60389e4c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ ## Introduction -Online Analyzer for Heap Dump, GC Log, and Thread Dump. +Online Analyzer for Heap Dump, GC Log, Thread Dump and JFR. Please refer to [GitHub Pages](https://eclipse.github.io/jifa) for more information. diff --git a/analysis/jfr/README.md b/analysis/jfr/README.md new file mode 100644 index 00000000..a3813475 --- /dev/null +++ b/analysis/jfr/README.md @@ -0,0 +1,21 @@ +# JFR Analysis + +Profiling is a type of runtime analysis. Java Flight Recorder (JFR) is the builtin Profiler for Java. It collects data on the fly and generates JFR files. JFR analysis gives you a birds-eye view of what is happening inside Java, such as CPU usage, memory allocation, and other activities of threads. + +### Supported Format + +- OpenJDK JFR (binary format) + +### Feature List + +The supported features are as follows: + +- CPU +- Allocation +- Wall Clock (Only JFR files created by async-profiler with wall engine) +- File IO +- Socket IO +- Lock +- Class Load +- Thread Sleep +- Native Execution Sample diff --git a/analysis/jfr/jfr.gradle b/analysis/jfr/jfr.gradle new file mode 100644 index 00000000..3cc852a2 --- /dev/null +++ b/analysis/jfr/jfr.gradle @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +plugins { + id 'java-library' +} + +apply from: "$rootDir/gradle/java.gradle" + +jar { + archiveBaseName.set("jfr") +} + +dependencies { + api project(':analysis') + implementation 'org.openjdk.jmc:flightrecorder:8.2.0' + implementation 'org.openjdk.jmc:flightrecorder.rules:8.2.0' + implementation 'org.openjdk.jmc:flightrecorder.rules.jdk:8.2.0' + implementation group: 'org.ow2.asm', name: 'asm', version: '9.3' +} \ No newline at end of file diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/JFRAnalysisApiExecutor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/JFRAnalysisApiExecutor.java new file mode 100644 index 00000000..74dce12a --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/JFRAnalysisApiExecutor.java @@ -0,0 +1,63 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jifa.analysis.AbstractApiExecutor; +import org.eclipse.jifa.analysis.listener.ProgressListener; +import org.eclipse.jifa.analysis.support.MethodNameConverter; +import org.eclipse.jifa.jfr.api.JFRAnalyzer; + +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Predicate; + +@Slf4j +public class JFRAnalysisApiExecutor extends AbstractApiExecutor { + @Override + public String namespace() { + return "jfr-file"; + } + + @Override + public Predicate matcher() { + return new Predicate<>() { + static final String HEADER = "FLR"; + + @Override + public boolean test(byte[] bytes) { + return bytes.length > HEADER.length() && new String(bytes, 0, HEADER.length()).equals(HEADER); + } + }; + } + + @Override + public boolean needOptionsForAnalysis(Path target) { + return false; + } + + @Override + public void clean(Path target) { + super.clean(target); + } + + @Override + protected MethodNameConverter methodNameConverter() { + return MethodNameConverter.GETTER_METHOD; + } + + @Override + protected JFRAnalyzer buildAnalyzer(Path target, Map options, ProgressListener listener) { + return new JFRAnalyzerImpl(target, options, listener); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/JFRAnalyzerImpl.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/JFRAnalyzerImpl.java new file mode 100644 index 00000000..a0670fba --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/JFRAnalyzerImpl.java @@ -0,0 +1,444 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jifa.analysis.listener.ProgressListener; +import org.eclipse.jifa.jfr.api.JFRAnalyzer; +import org.eclipse.jifa.jfr.exception.ProfileAnalysisException; +import org.eclipse.jifa.jfr.extractor.*; +import org.eclipse.jifa.jfr.extractor.Extractor; +import org.eclipse.jifa.jfr.extractor.JFRAnalysisContext; +import org.eclipse.jifa.jfr.common.ProfileDimension; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.request.AnalysisRequest; +import org.eclipse.jifa.jfr.request.DimensionBuilder; +import org.eclipse.jifa.jfr.model.*; +import org.eclipse.jifa.jfr.vo.Metadata; +import org.eclipse.jifa.jfr.vo.FlameGraph; +import org.openjdk.jmc.common.item.IItem; +import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.common.item.IItemIterable; +import org.openjdk.jmc.common.util.IPreferenceValueProvider; +import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit; +import org.openjdk.jmc.flightrecorder.rules.IResult; +import org.openjdk.jmc.flightrecorder.rules.IRule; +import org.openjdk.jmc.flightrecorder.rules.RuleRegistry; +import org.openjdk.jmc.flightrecorder.rules.Severity; + +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +@SuppressWarnings("unchecked") +@Slf4j +public class JFRAnalyzerImpl implements JFRAnalyzer { + + private final ProgressListener listener; + private final JFRAnalysisContext context; + + @Getter + private final AnalysisResult result; + + public JFRAnalyzerImpl(Path path, Map options, ProgressListener listener) { + this(path, DimensionBuilder.ALL, options, listener); + } + + public JFRAnalyzerImpl(Path path, int dimension, Map options, ProgressListener listener) { + AnalysisRequest request = new AnalysisRequest(path, dimension); + this.listener = listener; + this.context = new JFRAnalysisContext(request); + try { + this.result = this.execute(request); + } catch (RuntimeException t) { + throw t; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + @Override + public FlameGraph getFlameGraph(String dimension, boolean include, List taskSet) { + return createFlameGraph(ProfileDimension.of(dimension), result, include, taskSet); + } + + @Override + public Metadata metadata() { + Metadata basic = new Metadata(); + basic.setPerfDimensions(PerfDimensionFactory.PERF_DIMENSIONS); + return basic; + } + + private FlameGraph createFlameGraph(ProfileDimension dimension, AnalysisResult result, boolean include, + List taskSet) { + List os = new ArrayList<>(); + Map names = new HashMap<>(); + SymbolMap symbolTable = new SymbolMap(); + if (dimension == ProfileDimension.CPU) { + DimensionResult cpuTime = result.getCpuTime(); + generateCpuTime(cpuTime, os, names, symbolTable, include, taskSet); + } else { + DimensionResult DimensionResult = switch (dimension) { + case CPU_SAMPLE -> result.getCpuSample(); + case WALL_CLOCK -> result.getWallClock(); + case NATIVE_EXECUTION_SAMPLES -> result.getNativeExecutionSamples(); + case ALLOC -> result.getAllocations(); + case MEM -> result.getAllocatedMemory(); + case FILE_IO_TIME -> result.getFileIOTime(); + case FILE_READ_SIZE -> result.getFileReadSize(); + case FILE_WRITE_SIZE -> result.getFileWriteSize(); + case SOCKET_READ_TIME -> result.getSocketReadTime(); + case SOCKET_READ_SIZE -> result.getSocketReadSize(); + case SOCKET_WRITE_TIME -> result.getSocketWriteTime(); + case SOCKET_WRITE_SIZE -> result.getSocketWriteSize(); + case SYNCHRONIZATION -> result.getSynchronization(); + case THREAD_PARK -> result.getThreadPark(); + case CLASS_LOAD_COUNT -> result.getClassLoadCount(); + case CLASS_LOAD_WALL_TIME -> result.getClassLoadWallTime(); + case THREAD_SLEEP -> result.getThreadSleepTime(); + default -> throw new RuntimeException("should not reach here"); + }; + generate(DimensionResult, os, names, symbolTable, include, taskSet); + } + + FlameGraph fg = new FlameGraph(); + fg.setData(os.toArray(new Object[0][])); + fg.setThreadSplit(names); + fg.setSymbolTable(symbolTable.getReverseMap()); + return fg; + } + + private void generate(DimensionResult result, List os, Map names, + SymbolMap map, boolean include, List taskSet) { + List list = result.getList(); + Set set = null; + if (taskSet != null) { + set = new HashSet<>(taskSet); + } + for (TaskResultBase ts : list) { + if (set != null && !set.isEmpty()) { + if (include && !set.contains(ts.getTask().getName())) { + continue; + } else if (!include && set.contains(ts.getTask().getName())) { + continue; + } + } + this.doTaskResult(ts, os, names, map); + } + } + + private void doTaskResult(TaskResultBase taskResult, List os, Map names, SymbolMap map) { + Map samples = taskResult.getSamples(); + long total = 0; + for (StackTrace s : samples.keySet()) { + Frame[] frames = s.getFrames(); + String[] fs = new String[frames.length]; + for (int i = frames.length - 1, j = 0; i >= 0; i--, j++) { + fs[j] = frames[i].toString(); + } + Object[] o = new Object[3]; + o[0] = map.processSymbols(fs); + o[1] = samples.get(s); + o[2] = taskResult.getTask().getName(); + os.add(o); + total += samples.get(s); + } + names.put(taskResult.getTask().getName(), total); + } + + private static boolean isTaskNameIn(String taskName, List taskList) { + for (String name : taskList) { + if (taskName.contains(name)) { + return true; + } + } + return false; + } + + private void generateCpuTime(DimensionResult result, List os, + Map names, SymbolMap map, boolean include, List taskSet) { + List list = result.getList(); + for (TaskCPUTime ct : list) { + if (taskSet != null && !taskSet.isEmpty()) { + if (include) { + if (!isTaskNameIn(ct.getTask().getName(), taskSet)) { + continue; + } + } else { + if (isTaskNameIn(ct.getTask().getName(), taskSet)) { + continue; + } + } + } + + Map samples = ct.getSamples(); + if (samples != null && !samples.isEmpty()) { + long taskTotalTime = ct.getUser() + ct.getSystem(); + AtomicLong sampleCount = new AtomicLong(); + samples.values().forEach(sampleCount::addAndGet); + long perSampleTime = taskTotalTime / sampleCount.get(); + + for (StackTrace s : samples.keySet()) { + Frame[] frames = s.getFrames(); + String[] fs = new String[frames.length]; + for (int i = frames.length - 1, j = 0; i >= 0; i--, j++) { + fs[j] = frames[i].toString(); + } + Object[] o = new Object[3]; + o[0] = map.processSymbols(fs); + o[1] = samples.get(s) * perSampleTime; + o[2] = ct.getTask().getName(); + os.add(o); + } + + names.put(ct.getTask().getName(), taskTotalTime); + } + } + } + + private static class SymbolMap { + private final Map map = new HashMap<>(); + + String[] processSymbols(String[] fs) { + if (fs == null || fs.length == 0) { + return fs; + } + + String[] result = new String[fs.length]; + + synchronized (map) { + for (int i = 0; i < fs.length; i++) { + String symbol = fs[i]; + int id; + if (map.containsKey(symbol)) { + id = map.get(symbol); + } else { + id = map.size() + 1; + map.put(symbol, id); + } + result[i] = String.valueOf(id); + } + } + + return result; + } + + Map getReverseMap() { + Map reverseMap = new HashMap<>(); + map.forEach((key, value) -> reverseMap.put(value, key)); + return reverseMap; + } + } + + public AnalysisResult execute(AnalysisRequest request) throws ProfileAnalysisException { + try { + return analyze(request); + } catch (Exception e) { + if (e instanceof ProfileAnalysisException) { + throw (ProfileAnalysisException) e; + } + throw new ProfileAnalysisException(e); + } + } + + private AnalysisResult analyze(AnalysisRequest request) throws Exception { + listener.beginTask("Analyzing", 5); + long startTime = System.currentTimeMillis(); + AnalysisResult r = new AnalysisResult(); + + IItemCollection collection = this.loadEvents(request); + + this.analyzeProblemsIfNeeded(request, collection, r); + + this.transformEvents(request, collection); + + this.sortEvents(); + + this.processEvents(request, r); + + r.setProcessingTimeMillis(System.currentTimeMillis() - startTime); + log.info(String.format("Analysis took %d milliseconds", r.getProcessingTimeMillis())); + + return r; + } + + private void processEvents(AnalysisRequest request, AnalysisResult r) throws Exception { + listener.subTask("Do Extractors"); + List events = this.context.getEvents(); + final List extractors = getExtractors(request); + + if (request.getParallelWorkers() > 1) { + CountDownLatch countDownLatch = new CountDownLatch(extractors.size()); + ExecutorService es = Executors.newFixedThreadPool(request.getParallelWorkers()); + extractors.forEach(item -> es.submit(() -> { + try { + doExtractorWork(events, item, r); + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + countDownLatch.countDown(); + } + })); + countDownLatch.await(); + es.shutdown(); + } else { + extractors.forEach(item -> { + doExtractorWork(events, item, r); + }); + } + listener.worked(1); + } + + private void doExtractorWork(List events, Extractor extractor, AnalysisResult r) { + events.forEach(extractor::process); + extractor.fillResult(r); + } + + private List getExtractors(AnalysisRequest request) { + return getExtractors(request.getDimensions()); + } + + private void sortEvents() { + listener.subTask("Sort Events"); + this.context.getEvents().sort(Comparator.comparing(RecordedEvent::getStartTime)); + listener.worked(1); + } + + private void transformEvents(AnalysisRequest request, IItemCollection collection) throws Exception { + listener.subTask("Transform Events"); + List list = collection.stream().flatMap(IItemIterable::stream).collect(Collectors.toList()); + + if (request.getParallelWorkers() > 1) { + parseEventsParallel(list, request.getParallelWorkers()); + } else { + list.forEach(this::parseEventItem); + } + + listener.worked(1); + } + + private IItemCollection loadEvents(AnalysisRequest request) throws Exception { + try { + listener.subTask("Load Events"); + if (request.getInput() != null) { + return JfrLoaderToolkit.loadEvents(request.getInput().toFile()); + } else { + return JfrLoaderToolkit.loadEvents(request.getInputStream()); + } + } finally { + listener.worked(1); + } + } + + private void analyzeProblemsIfNeeded(AnalysisRequest request, IItemCollection collection, AnalysisResult r) { + listener.subTask("Analyze Problems"); + if ((request.getDimensions() & ProfileDimension.PROBLEMS.getValue()) != 0) { + this.analyzeProblems(collection, r); + } + listener.worked(1); + } + + private void parseEventsParallel(List list, int workers) throws Exception { + listener.subTask("Transform Events"); + CountDownLatch countDownLatch = new CountDownLatch(list.size()); + ExecutorService es = Executors.newFixedThreadPool(workers); + list.forEach(item -> es.submit(() -> { + try { + this.parseEventItem(item); + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + countDownLatch.countDown(); + } + })); + countDownLatch.await(); + es.shutdown(); + listener.worked(workers); + } + + private void analyzeProblems(IItemCollection collection, AnalysisResult r) { + r.setProblems(new ArrayList<>()); + for (IRule rule : RuleRegistry.getRules()) { + RunnableFuture future; + try { + future = rule.createEvaluation(collection, IPreferenceValueProvider.DEFAULT_VALUES, null); + future.run(); + IResult result = future.get(); + Severity severity = result.getSeverity(); + if (severity == Severity.WARNING) { + r.getProblems().add(new Problem(result.getSummary(), result.getSolution())); + } + } catch (Throwable t) { + log.error("Failed to run jmc rule {}", rule.getName()); + } + } + } + + private void parseEventItem(IItem item) { + RecordedEvent event = RecordedEvent.newInstance(item, this.context.getSymbols()); + + synchronized (this.context.getEvents()) { + this.context.addEvent(event); + if (event.getSettingFor() != null) { + RecordedEvent.SettingFor sf = event.getSettingFor(); + this.context.putEventTypeId(sf.getEventType(), sf.getEventId()); + } + } + } + + private List getExtractors(int dimensions) { + List extractors = new ArrayList<>(); + Map extractorMap = new HashMap<>() { + { + put(DimensionBuilder.CPU, new CPUTimeExtractor(context)); + put(DimensionBuilder.CPU_SAMPLE, new CPUSampleExtractor(context)); + put(DimensionBuilder.WALL_CLOCK, new WallClockExtractor(context)); + put(DimensionBuilder.NATIVE_EXECUTION_SAMPLES, new NativeExecutionExtractor(context)); + put(DimensionBuilder.ALLOC, new AllocationsExtractor(context)); + put(DimensionBuilder.MEM, new AllocatedMemoryExtractor(context)); + + put(DimensionBuilder.FILE_IO_TIME, new FileIOTimeExtractor(context)); + put(DimensionBuilder.FILE_READ_SIZE, new FileReadExtractor(context)); + put(DimensionBuilder.FILE_WRITE_SIZE, new FileWriteExtractor(context)); + + put(DimensionBuilder.SOCKET_READ_TIME, new SocketReadTimeExtractor(context)); + put(DimensionBuilder.SOCKET_READ_SIZE, new SocketReadSizeExtractor(context)); + put(DimensionBuilder.SOCKET_WRITE_TIME, new SocketWriteTimeExtractor(context)); + put(DimensionBuilder.SOCKET_WRITE_SIZE, new SocketWriteSizeExtractor(context)); + + put(DimensionBuilder.SYNCHRONIZATION, new SynchronizationExtractor(context)); + put(DimensionBuilder.THREAD_PARK, new ThreadParkExtractor(context)); + + put(DimensionBuilder.CLASS_LOAD_COUNT, new ClassLoadCountExtractor(context)); + put(DimensionBuilder.CLASS_LOAD_WALL_TIME, new ClassLoadWallTimeExtractor(context)); + + put(DimensionBuilder.THREAD_SLEEP, new ThreadSleepTimeExtractor(context)); + } + }; + + extractorMap.keySet().forEach(item -> { + if ((dimensions & item) != 0) { + extractors.add(extractorMap.get(item)); + } + }); + + return extractors; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/PerfDimensionFactory.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/PerfDimensionFactory.java new file mode 100644 index 00000000..112a4879 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/PerfDimensionFactory.java @@ -0,0 +1,88 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr; + +import org.eclipse.jifa.jfr.enums.Unit; +import org.eclipse.jifa.jfr.common.ProfileDimension; +import org.eclipse.jifa.jfr.model.Filter; +import org.eclipse.jifa.jfr.model.PerfDimension; + +public class PerfDimensionFactory { + + public static PerfDimension[] PERF_DIMENSIONS; + + static final Filter FILTER_THREAD = Filter.of("Thread", null); + static final Filter FILTER_CLASS = Filter.of("Class", null); + static final Filter FILTER_METHOD = Filter.of("Method", null); + + static final Filter[] FILTERS = new Filter[]{FILTER_THREAD, FILTER_CLASS, FILTER_METHOD}; + + static final PerfDimension DIM_CPU_TIME = PerfDimension.of(ProfileDimension.CPU.getKey(), ProfileDimension.CPU.getDesc(), FILTERS, Unit.NANO_SECOND); + + static final PerfDimension DIM_CPU_SAMPLE = PerfDimension.of(ProfileDimension.CPU_SAMPLE.getKey(), ProfileDimension.CPU_SAMPLE.getDesc(), FILTERS, Unit.COUNT); + + static final PerfDimension DIM_WALL_CLOCK = PerfDimension.of(ProfileDimension.WALL_CLOCK.getKey(), ProfileDimension.WALL_CLOCK.getDesc(), FILTERS, Unit.NANO_SECOND); + + static final PerfDimension DIM_NATIVE_EXECUTION_SAMPLES = PerfDimension.of(ProfileDimension.NATIVE_EXECUTION_SAMPLES.getKey(), ProfileDimension.NATIVE_EXECUTION_SAMPLES.getDesc(), FILTERS); + + static final PerfDimension DIM_ALLOC_COUNT = PerfDimension.of(ProfileDimension.ALLOC.getKey(), ProfileDimension.ALLOC.getDesc(), FILTERS, Unit.COUNT); + + static final PerfDimension DIM_ALLOC_MEMORY = PerfDimension.of(ProfileDimension.MEM.getKey(), ProfileDimension.MEM.getDesc(), FILTERS, Unit.BYTE); + + static final PerfDimension DIM_FILE_IO_TIME = PerfDimension.of(ProfileDimension.FILE_IO_TIME.getKey(), ProfileDimension.FILE_IO_TIME.getDesc(), FILTERS, Unit.NANO_SECOND); + + static final PerfDimension DIM_FILE_READ_SIZE = PerfDimension.of(ProfileDimension.FILE_READ_SIZE.getKey(), ProfileDimension.FILE_READ_SIZE.getDesc(), FILTERS, Unit.BYTE); + + static final PerfDimension DIM_FILE_WRITE_SIZE = PerfDimension.of(ProfileDimension.FILE_WRITE_SIZE.getKey(), ProfileDimension.FILE_WRITE_SIZE.getDesc(), FILTERS, Unit.BYTE); + + static final PerfDimension DIM_SOCKET_READ_TIME = PerfDimension.of(ProfileDimension.SOCKET_READ_TIME.getKey(), ProfileDimension.SOCKET_READ_TIME.getDesc(), FILTERS, Unit.NANO_SECOND); + + static final PerfDimension DIM_SOCKET_READ_SIZE = PerfDimension.of(ProfileDimension.SOCKET_READ_SIZE.getKey(), ProfileDimension.SOCKET_READ_SIZE.getDesc(), FILTERS, Unit.BYTE); + + static final PerfDimension DIM_SOCKET_WRITE_TIME = PerfDimension.of(ProfileDimension.SOCKET_WRITE_TIME.getKey(), ProfileDimension.SOCKET_WRITE_TIME.getDesc(), FILTERS, Unit.NANO_SECOND); + + static final PerfDimension DIM_SOCKET_WRITE_SIZE = PerfDimension.of(ProfileDimension.SOCKET_WRITE_SIZE.getKey(), ProfileDimension.SOCKET_WRITE_SIZE.getDesc(), FILTERS, Unit.BYTE); + + static final PerfDimension DIM_SYNCHRONIZATION = PerfDimension.of(ProfileDimension.SYNCHRONIZATION.getKey(), ProfileDimension.SYNCHRONIZATION.getDesc(), FILTERS, Unit.NANO_SECOND); + + static final PerfDimension DIM_THREAD_PARK = PerfDimension.of(ProfileDimension.THREAD_PARK.getKey(), ProfileDimension.THREAD_PARK.getDesc(), FILTERS, Unit.NANO_SECOND); + + static final PerfDimension DIM_CLASS_LOAD_WALL_TIME = PerfDimension.of(ProfileDimension.CLASS_LOAD_WALL_TIME.getKey(), ProfileDimension.CLASS_LOAD_WALL_TIME.getDesc(), FILTERS, Unit.NANO_SECOND); + + static final PerfDimension DIM_CLASS_LOAD_COUNT = PerfDimension.of(ProfileDimension.CLASS_LOAD_COUNT.getKey(), ProfileDimension.CLASS_LOAD_COUNT.getDesc(), FILTERS, Unit.COUNT); + + static final PerfDimension DIM_THREAD_SLEEP_TIME = PerfDimension.of(ProfileDimension.THREAD_SLEEP.getKey(), ProfileDimension.THREAD_SLEEP.getDesc(), FILTERS, Unit.NANO_SECOND); + + static { + PERF_DIMENSIONS = new PerfDimension[]{ + DIM_CPU_TIME, + DIM_CPU_SAMPLE, + DIM_WALL_CLOCK, + DIM_NATIVE_EXECUTION_SAMPLES, + DIM_ALLOC_COUNT, + DIM_ALLOC_MEMORY, + DIM_FILE_IO_TIME, + DIM_FILE_READ_SIZE, + DIM_FILE_WRITE_SIZE, + DIM_SOCKET_READ_TIME, + DIM_SOCKET_READ_SIZE, + DIM_SOCKET_WRITE_TIME, + DIM_SOCKET_WRITE_SIZE, + DIM_SYNCHRONIZATION, + DIM_THREAD_PARK, + DIM_CLASS_LOAD_WALL_TIME, + DIM_CLASS_LOAD_COUNT, + DIM_THREAD_SLEEP_TIME, + }; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/api/JFRAnalyzer.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/api/JFRAnalyzer.java new file mode 100644 index 00000000..c5b68e7e --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/api/JFRAnalyzer.java @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +package org.eclipse.jifa.jfr.api; + +import org.eclipse.jifa.jfr.vo.Metadata; +import org.eclipse.jifa.jfr.vo.FlameGraph; + +import java.util.List; + +public interface JFRAnalyzer { + Metadata metadata(); + FlameGraph getFlameGraph(String dimension, boolean include, List taskSet); +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/common/EventConstant.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/common/EventConstant.java new file mode 100644 index 00000000..1be02f28 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/common/EventConstant.java @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +package org.eclipse.jifa.jfr.common; + +public abstract class EventConstant { + public static String UNSIGNED_INT_FLAG = "jdk.UnsignedIntFlag"; + public static String GARBAGE_COLLECTION = "jdk.GarbageCollection"; + + public static String CPU_INFORMATION = "jdk.CPUInformation"; + public static String CPC_RUNTIME_INFORMATION = "cpc.RuntimeInformation"; + public static String ENV_VAR = "jdk.InitialEnvironmentVariable"; + + public static String PROCESS_CPU_LOAD = "jdk.CPULoad"; + public static String ACTIVE_SETTING = "jdk.ActiveSetting"; + + public static String THREAD_START = "jdk.ThreadStart"; + public static String THREAD_CPU_LOAD = "jdk.ThreadCPULoad"; + public static String EXECUTION_SAMPLE = "jdk.ExecutionSample"; + public static String WALL_CLOCK_SAMPLE = "jdk.ExecutionSample"; + public static String NATIVE_EXECUTION_SAMPLE = "jdk.NativeMethodSample"; + public static String EXECUTE_VM_OPERATION = "jdk.ExecuteVMOperation"; + + public static String OBJECT_ALLOCATION_SAMPLE = "jdk.ObjectAllocationSample"; // TODO + public static String OBJECT_ALLOCATION_IN_NEW_TLAB = "jdk.ObjectAllocationInNewTLAB"; + public static String OBJECT_ALLOCATION_OUTSIDE_TLAB = "jdk.ObjectAllocationOutsideTLAB"; + + public static String FILE_WRITE = "jdk.FileWrite"; + public static String FILE_READ = "jdk.FileRead"; + public static String FILE_FORCE = "jdk.FileForce"; + + public static String SOCKET_READ = "jdk.SocketRead"; + public static String SOCKET_WRITE = "jdk.SocketWrite"; + + public static String JAVA_MONITOR_ENTER = "jdk.JavaMonitorEnter"; + public static String JAVA_MONITOR_WAIT = "jdk.JavaMonitorWait"; + public static String THREAD_PARK = "jdk.ThreadPark"; + + public static String CLASS_LOAD = "jdk.ClassLoad"; + + public static String THREAD_SLEEP = "jdk.ThreadSleep"; + + public static String PERIOD = "period"; + + public static String INTERVAL = "interval"; + public static String WALL = "wall"; + public static String EVENT = "event"; +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/common/ProfileDimension.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/common/ProfileDimension.java new file mode 100644 index 00000000..59bdaafe --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/common/ProfileDimension.java @@ -0,0 +1,79 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.common; + +import lombok.Getter; + +public enum ProfileDimension { + CPU(1, "CPU Time"), + CPU_SAMPLE(1 << 1, "CPU Sample"), + WALL_CLOCK(1 << 2, "Wall Clock"), + NATIVE_EXECUTION_SAMPLES(1 << 3, "Native Execution Samples"), + ALLOC(1 << 4, "Allocation Count"), + MEM(1 << 5, "Allocated Memory"), + + FILE_IO_TIME(1 << 6, "File IO Time"), + FILE_READ_SIZE(1 << 7, "File Read Size"), + FILE_WRITE_SIZE(1 << 8, "File Write Size"), + + SOCKET_READ_SIZE(1 << 9, "Socket Read Size"), + SOCKET_WRITE_SIZE(1 << 10, "Socket Write Size"), + SOCKET_READ_TIME(1 << 11, "Socket Read Time"), + SOCKET_WRITE_TIME(1 << 12, "Socket Write Time"), + + SYNCHRONIZATION(1 << 13, "Synchronization"), + THREAD_PARK(1 << 14, "Thread Park"), + + CLASS_LOAD_COUNT(1 << 15, "Class Load Count"), + CLASS_LOAD_WALL_TIME(1 << 16, "Class Load Wall Time"), + + THREAD_SLEEP(1 << 17, "Thread Sleep Time"), + + PROBLEMS(1 << 20, "Problem"); + + @Getter + private final int value; + + @Getter + private final String key; + + @Getter + private final String desc; + + ProfileDimension(int v, String key) { + this.value = v; + this.key = key; + this.desc = key; + } + + public static ProfileDimension of(String key) { + for (ProfileDimension f : ProfileDimension.values()) { + if (f.key.equalsIgnoreCase(key)) { + return f; + } + } + throw new RuntimeException("invalid profile dimension key [" + key + "]"); + } + + public boolean active(int dimensions) { + return (dimensions & this.value) != 0; + } + + public static int of(ProfileDimension... dimensions) { + int r = 0; + for (ProfileDimension dimension : dimensions) { + r |= dimension.value; + } + return r; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/enums/Unit.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/enums/Unit.java new file mode 100644 index 00000000..34c2e98f --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/enums/Unit.java @@ -0,0 +1,34 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.enums; + +import org.eclipse.jifa.common.annotation.UseGsonEnumAdaptor; + +@UseGsonEnumAdaptor +public enum Unit { + NANO_SECOND("ns"), + + BYTE("byte"), + + COUNT("count"); + + private final String tag; + + Unit(String tag) { + this.tag = tag; + } + + public String toString() { + return tag; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/exception/ProfileAnalysisException.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/exception/ProfileAnalysisException.java new file mode 100644 index 00000000..8287501f --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/exception/ProfileAnalysisException.java @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.exception; + +public class ProfileAnalysisException extends Exception { + public ProfileAnalysisException(String message, Throwable cause) { + super(message, cause); + } + + public ProfileAnalysisException(Throwable cause) { + super(cause); + } + + public ProfileAnalysisException(String message) { + super(message); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/AllocatedMemoryExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/AllocatedMemoryExtractor.java new file mode 100644 index 00000000..32e70bc6 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/AllocatedMemoryExtractor.java @@ -0,0 +1,111 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.jfr.RecordedStackTrace; +import org.eclipse.jifa.jfr.util.StackTraceUtil; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.Task; +import org.eclipse.jifa.jfr.model.TaskAllocatedMemory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class AllocatedMemoryExtractor extends AllocationsExtractor { + public AllocatedMemoryExtractor(JFRAnalysisContext context) { + super(context); + } + + @Override + void visitObjectAllocationInNewTLAB(RecordedEvent event) { + RecordedStackTrace stackTrace = event.getStackTrace(); + if (stackTrace == null) { + return; + } + + AllocationsExtractor.AllocTaskData allocThreadData = getThreadData(event.getThread()); + if (allocThreadData.getSamples() == null) { + allocThreadData.setSamples(new HashMap<>()); + } + + long eventTotal = event.getLong("tlabSize"); + + allocThreadData.getSamples().compute(stackTrace, (k, temp) -> temp == null ? eventTotal : temp + eventTotal); + allocThreadData.allocatedMemory += eventTotal; + } + + @Override + void visitObjectAllocationOutsideTLAB(RecordedEvent event) { + RecordedStackTrace stackTrace = event.getStackTrace(); + if (stackTrace == null) { + return; + } + + AllocTaskData allocThreadData = getThreadData(event.getThread()); + if (allocThreadData.getSamples() == null) { + allocThreadData.setSamples(new HashMap<>()); + } + + long eventTotal = event.getLong("allocationSize"); + + allocThreadData.getSamples().compute(stackTrace, (k, temp) -> temp == null ? eventTotal : temp + eventTotal); + allocThreadData.allocatedMemory += eventTotal; + } + + private List buildThreadAllocatedMemory() { + List taskAllocatedMemoryList = new ArrayList<>(); + + for (AllocTaskData data : this.data.values()) { + if (data.allocatedMemory == 0) { + continue; + } + + TaskAllocatedMemory taskAllocatedMemory = new TaskAllocatedMemory(); + Task ta = new Task(); + ta.setId(data.getThread().getJavaThreadId()); + ta.setName(data.getThread().getJavaName()); + taskAllocatedMemory.setTask(ta); + + if (data.getSamples() != null) { + taskAllocatedMemory.setAllocatedMemory(data.allocatedMemory); + taskAllocatedMemory.setSamples(data.getSamples().entrySet().stream().collect( + Collectors.toMap( + e -> StackTraceUtil.build(e.getKey(), context.getSymbols()), + Map.Entry::getValue, + Long::sum) + )); + } + + taskAllocatedMemoryList.add(taskAllocatedMemory); + } + + taskAllocatedMemoryList.sort((o1, o2) -> { + long delta = o2.getAllocatedMemory() - o1.getAllocatedMemory(); + return delta > 0 ? 1 : (delta == 0 ? 0 : -1); + }); + + return taskAllocatedMemoryList; + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult memResult = new DimensionResult<>(); + memResult.setList(buildThreadAllocatedMemory()); + result.setAllocatedMemory(memResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/AllocationsExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/AllocationsExtractor.java new file mode 100644 index 00000000..b31c337f --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/AllocationsExtractor.java @@ -0,0 +1,119 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.TaskData; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.jfr.RecordedStackTrace; +import org.eclipse.jifa.jfr.model.jfr.RecordedThread; +import org.eclipse.jifa.jfr.util.StackTraceUtil; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.Task; +import org.eclipse.jifa.jfr.model.TaskAllocations; + +import java.util.*; +import java.util.stream.Collectors; + +public class AllocationsExtractor extends Extractor { + + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList() { + { + add(EventConstant.OBJECT_ALLOCATION_IN_NEW_TLAB); + add(EventConstant.OBJECT_ALLOCATION_OUTSIDE_TLAB); + } + }); + + protected static class AllocTaskData extends TaskData { + AllocTaskData(RecordedThread thread) { + super(thread); + } + + public long allocations; + + public long allocatedMemory; + } + + protected final Map data = new HashMap<>(); + + public AllocationsExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + AllocTaskData getThreadData(RecordedThread thread) { + return data.computeIfAbsent(thread.getJavaThreadId(), i -> new AllocTaskData(thread)); + } + + @Override + void visitObjectAllocationInNewTLAB(RecordedEvent event) { + RecordedStackTrace stackTrace = event.getStackTrace(); + if (stackTrace == null) { + return; + } + + AllocTaskData allocThreadData = getThreadData(event.getThread()); + if (allocThreadData.getSamples() == null) { + allocThreadData.setSamples(new HashMap<>()); + } + + allocThreadData.getSamples().compute(stackTrace, (k, count) -> count == null ? 1 : count + 1); + allocThreadData.allocations += 1; + } + + @Override + void visitObjectAllocationOutsideTLAB(RecordedEvent event) { + this.visitObjectAllocationInNewTLAB(event); + } + + private List buildThreadAllocations() { + List taskAllocations = new ArrayList<>(); + for (AllocTaskData data : this.data.values()) { + if (data.allocations == 0) { + continue; + } + + TaskAllocations threadAllocation = new TaskAllocations(); + Task ta = new Task(); + ta.setId(data.getThread().getJavaThreadId()); + ta.setName(data.getThread().getJavaName()); + threadAllocation.setTask(ta); + + if (data.getSamples() != null) { + threadAllocation.setAllocations(data.allocations); + threadAllocation.setSamples(data.getSamples().entrySet().stream().collect( + Collectors.toMap( + e -> StackTraceUtil.build(e.getKey(), context.getSymbols()), + Map.Entry::getValue, + Long::sum) + )); + } + + taskAllocations.add(threadAllocation); + } + + taskAllocations.sort((o1, o2) -> { + long delta = o2.getAllocations() - o1.getAllocations(); + return delta > 0 ? 1 : (delta == 0 ? 0 : -1); + }); + + return taskAllocations; + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult allocResult = new DimensionResult<>(); + allocResult.setList(buildThreadAllocations()); + result.setAllocations(allocResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/CPUSampleExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/CPUSampleExtractor.java new file mode 100644 index 00000000..33f4ce45 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/CPUSampleExtractor.java @@ -0,0 +1,69 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.TaskCount; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CPUSampleExtractor extends CountExtractor { + private boolean isWallClockEvents = false; + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList() { + { + add(EventConstant.EXECUTION_SAMPLE); + add(EventConstant.ACTIVE_SETTING); + } + }); + + public CPUSampleExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitExecutionSample(RecordedEvent event) { + visitEvent(event); + } + + @Override + void visitActiveSetting(RecordedEvent event) { + if (this.context.isExecutionSampleEventTypeId(event.getSettingFor().getEventId())) { + if (EventConstant.WALL.equals(event.getString("name"))) { + this.isWallClockEvents = true; + } + } + if (EventConstant.EVENT.equals(event.getString("name")) && EventConstant.WALL.equals(event.getString("value"))) { + this.isWallClockEvents = true; + } + } + + public List buildTaskCounts() { + if (this.isWallClockEvents) { + return new ArrayList<>(); + } else { + return super.buildTaskCounts(); + } + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskCounts()); + result.setCpuSample(tsResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/CPUTimeExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/CPUTimeExtractor.java new file mode 100644 index 00000000..d2587e92 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/CPUTimeExtractor.java @@ -0,0 +1,334 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.*; +import org.eclipse.jifa.jfr.model.*; +import org.eclipse.jifa.jfr.util.GCUtil; +import org.eclipse.jifa.jfr.util.StackTraceUtil; +import org.eclipse.jifa.jfr.util.TimeUtil; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.JavaThreadCPUTime; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +public class CPUTimeExtractor extends Extractor { + + private static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.UNSIGNED_INT_FLAG); + add(EventConstant.GARBAGE_COLLECTION); + add(EventConstant.ACTIVE_SETTING); + add(EventConstant.CPU_INFORMATION); + add(EventConstant.ENV_VAR); + add(EventConstant.THREAD_START); + add(EventConstant.THREAD_CPU_LOAD); + add(EventConstant.EXECUTION_SAMPLE); + } + }); + + private static class CpuTaskData extends TaskData { + CpuTaskData(RecordedThread thread) { + super(thread); + } + + Instant start; + + long user = 0; + + long system = 0; + + long sampleCount; + + boolean firstThreadCPULoadEventIsFired; + } + + private static final int ASYNC_PROFILER_DEFAULT_INTERVAL = 10 * 1000 * 1000; + private final Map data = new HashMap<>(); + + private long period = -1; + + private long threadCPULoadEventId = -1; + private boolean profiledByJFR = true; + + private int cpuCores; + private long intervalAsyncProfiler; // unit: nano + private long intervalJFR; // unit: nano + + private int concurrentGCThreads = -1; + private int parallelGCThreads = -1; + private long concurrentGCWallTime = 0; + private long parallelGCWallTime = 0; + private long serialGCWallTime = 0; + + private boolean isWallClockEvents = false; + + private static final RecordedStackTrace DUMMY_STACK_TRACE = newDummyStackTrace("", "", "NO Frame"); + private static final RecordedThread DUMMY_THREAD = new RecordedThread("Dummy Thread", -1L, -1L); + private static final RecordedThread GC_THREAD = new RecordedThread("GC Thread", -10L, -10L); + + public CPUTimeExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + + Long id = context.getEventTypeId(EventConstant.THREAD_CPU_LOAD); + if (id != null) { + threadCPULoadEventId = id; + } + } + + CpuTaskData getThreadData(RecordedThread thread) { + return data.computeIfAbsent(thread.getJavaThreadId(), i -> new CpuTaskData(thread)); + } + + private void updatePeriod(String value) { + period = TimeUtil.parseTimespan(value); + } + + @Override + void visitUnsignedIntFlag(RecordedEvent event) { + String name = event.getString("name"); + if ("ConcGCThreads".equals(name)) { + concurrentGCThreads = event.getInt("value"); + } else if ("ParallelGCThreads".equals(name)) { + parallelGCThreads = event.getInt("value"); + } + } + + @Override + void visitGarbageCollection(RecordedEvent event) { + String name = event.getString("name"); + long duration = event.getDuration().toNanos(); + + if (GCUtil.isParallelGC(name)) { + parallelGCWallTime += duration; + } else if (GCUtil.isConcGC(name)) { + concurrentGCWallTime += duration; + } else if (GCUtil.isSerialGC(name)) { + serialGCWallTime += duration; + } + } + + @Override + void visitActiveSetting(RecordedEvent event) { + if (event.getSettingFor().getEventId() == threadCPULoadEventId + && EventConstant.PERIOD.equals(event.getString("name"))) { + updatePeriod(event.getValue("value")); + } + + if (EventConstant.EVENT.equals(event.getString("name")) && EventConstant.WALL.equals(event.getString("value"))) { + this.isWallClockEvents = true; + } + + if (this.context.isExecutionSampleEventTypeId(event.getSettingFor().getEventId())) { + if (EventConstant.WALL.equals(event.getString("name"))) { + this.isWallClockEvents = true; + } else if (EventConstant.INTERVAL.equals(event.getString("name"))) { + // async-profiler is "interval" + this.intervalAsyncProfiler = Long.parseLong(event.getString("value")); + this.profiledByJFR = false; + } else if (EventConstant.PERIOD.equals(event.getString("name"))) { + // JFR is "period" + try { + this.intervalJFR = TimeUtil.parseTimespan(event.getString("value")); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + } + } + + @Override + void visitCPUInformation(RecordedEvent event) { + if (cpuCores == 0) { + cpuCores = event.getInt("hwThreads"); + } + } + + @Override + void visitEnvVar(RecordedEvent event) { + if ("CPU_COUNT".equals(event.getString("key"))) { + cpuCores = Integer.parseInt(event.getString("value")); + } + } + + @Override + void visitThreadStart(RecordedEvent event) { + if (event.getThread() == null) { + return; + } + CpuTaskData cpuTaskData = getThreadData(event.getThread()); + cpuTaskData.start = event.getStartTime(); + } + + @Override + void visitThreadCPULoad(RecordedEvent event) { + if (event.getThread() == null) { + return; + } + CpuTaskData cpuTaskData = getThreadData(event.getThread()); + long nanos = period; + if (!cpuTaskData.firstThreadCPULoadEventIsFired) { + if (cpuTaskData.start != null) { + Duration between = Duration.between(cpuTaskData.start, event.getStartTime()); + nanos = Math.min(nanos, between.toNanos()); + } + cpuTaskData.firstThreadCPULoadEventIsFired = true; + } + cpuTaskData.user += (long) (event.getFloat("user") * nanos); + cpuTaskData.system += (long) (event.getFloat("system") * nanos); + } + + @Override + void visitExecutionSample(RecordedEvent event) { + RecordedStackTrace stackTrace = event.getStackTrace(); + if (stackTrace == null) { + stackTrace = DUMMY_STACK_TRACE; + } + + RecordedThread thread = event.getThread("eventThread"); + if (thread == null) { + thread = event.getThread("sampledThread"); + } + if (thread == null) { + thread = DUMMY_THREAD; + } + CpuTaskData cpuTaskData = getThreadData(thread); + + if (cpuTaskData.getSamples() == null) { + cpuTaskData.setSamples(new HashMap<>()); + } + + cpuTaskData.getSamples().compute(stackTrace, (k, count) -> count == null ? 1 : count + 1); + cpuTaskData.sampleCount++; + } + + private List buildThreadCPUTime() { + List threadCPUTimes = new ArrayList<>(); + if (this.isWallClockEvents) { + return threadCPUTimes; + } + for (CpuTaskData data : this.data.values()) { + if (data.getSamples() == null) { + continue; + } + JavaThreadCPUTime threadCPUTime = new JavaThreadCPUTime(); + threadCPUTime.setTask(context.getThread(data.getThread())); + + if (data.getSamples() != null) { + if (this.profiledByJFR) { + if (intervalJFR <= 0) { + throw new RuntimeException("need profiling interval to calculate approximate CPU time"); + } + long cpuTimeMax = (data.user + data.system) * cpuCores; + long sampleTime = data.sampleCount * intervalJFR; + if (cpuTimeMax == 0) { + threadCPUTime.setUser(sampleTime); + } else { + threadCPUTime.setUser(Math.min(sampleTime, cpuTimeMax)); + } + threadCPUTime.setSystem(0); + } else { + if (intervalAsyncProfiler <= 0) { + intervalAsyncProfiler = detectAsyncProfilerInterval(); + } + threadCPUTime.setUser(data.sampleCount * intervalAsyncProfiler); + threadCPUTime.setSystem(0); + } + + threadCPUTime.setSamples(data.getSamples().entrySet().stream().collect( + Collectors.toMap( + e -> StackTraceUtil.build(e.getKey(), context.getSymbols()), + Map.Entry::getValue, + Long::sum) + )); + } + + threadCPUTimes.add(threadCPUTime); + } + + if (this.profiledByJFR) { + long gcTime = buildGCCpuTime(); + if (gcTime > 0) { + JavaThreadCPUTime gc = new JavaThreadCPUTime(); + gc.setTask(context.getThread(GC_THREAD)); + gc.setUser(gcTime); + Map gcSamples = new HashMap<>(); + gcSamples.put(StackTraceUtil.build(newDummyStackTrace("", "JVM", "GC"), context.getSymbols()), 1L); + gc.setSamples(gcSamples); + threadCPUTimes.add(gc); + } + } + + threadCPUTimes.sort((o1, o2) -> { + long delta = o2.totalCPUTime() - o1.totalCPUTime(); + return delta > 0 ? 1 : (delta == 0 ? 0 : -1); + }); + return threadCPUTimes; + } + + private long buildGCCpuTime() { + if (parallelGCThreads < 0 || concurrentGCThreads < 0) { + log.warn("invalid ParallelGCThreads or ConcurrentGCThreads, GC cpu time can not be calculated"); + return -1; + } else { + return parallelGCThreads * parallelGCWallTime + concurrentGCThreads * concurrentGCWallTime + serialGCWallTime; + } + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult cpuResult = new DimensionResult<>(); + List list = buildThreadCPUTime(); + cpuResult.setList(list); + result.setCpuTime(cpuResult); + } + + private static RecordedStackTrace newDummyStackTrace(String packageName, String className, String methodName) { + RecordedStackTrace st = new RecordedStackTrace(); + List list = new ArrayList<>(); + RecordedFrame f = new RecordedFrame(); + RecordedMethod m = new RecordedMethod(); + RecordedClass c = new RecordedClass(); + c.setPackageName(packageName); + c.setName(className); + m.setType(c); + f.setMethod(m); + m.setName(methodName); + list.add(f); + st.setFrames(list); + return st; + } + + private static long detectAsyncProfilerInterval() { + long interval = 0; + String intervalStr = System.getProperty("asyncProfilerCpuIntervalMs"); + if (intervalStr != null && !intervalStr.isEmpty()) { + try { + interval = Long.parseLong(intervalStr) * 1000 * 1000; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + if (interval <= 0) { + log.info("use default cpu interval 10ms"); + interval = ASYNC_PROFILER_DEFAULT_INTERVAL; + } + return interval; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ClassLoadCountExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ClassLoadCountExtractor.java new file mode 100644 index 00000000..c7aeb60c --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ClassLoadCountExtractor.java @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.TaskCount; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ClassLoadCountExtractor extends CountExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.CLASS_LOAD); + } + }); + + public ClassLoadCountExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitClassLoad(RecordedEvent event) { + visitEvent(event); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskCounts()); + result.setClassLoadCount(tsResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ClassLoadWallTimeExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ClassLoadWallTimeExtractor.java new file mode 100644 index 00000000..9b0a2194 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ClassLoadWallTimeExtractor.java @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ClassLoadWallTimeExtractor extends SumExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.CLASS_LOAD); + } + }); + + public ClassLoadWallTimeExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitClassLoad(RecordedEvent event) { + visitEvent(event, event.getDurationNano()); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskSums()); + result.setClassLoadWallTime(tsResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/CountExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/CountExtractor.java new file mode 100644 index 00000000..75901c2d --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/CountExtractor.java @@ -0,0 +1,96 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.util.StackTraceUtil; +import org.eclipse.jifa.jfr.model.TaskData; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.jfr.RecordedStackTrace; +import org.eclipse.jifa.jfr.model.jfr.RecordedThread; +import org.eclipse.jifa.jfr.model.TaskCount; +import org.eclipse.jifa.jfr.model.Task; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public abstract class CountExtractor extends Extractor { + CountExtractor(JFRAnalysisContext context, List interested) { + super(context, interested); + } + + public static class TaskCountData extends TaskData { + TaskCountData(RecordedThread thread) { + super(thread); + } + + long count; + } + + protected final Map data = new HashMap<>(); + + TaskCountData getTaskCountData(RecordedThread thread) { + return data.computeIfAbsent(thread.getJavaThreadId(), i -> new TaskCountData(thread)); + } + + protected void visitEvent(RecordedEvent event) { + RecordedStackTrace stackTrace = event.getStackTrace(); + if (stackTrace == null) { + return; + } + + TaskCountData data = getTaskCountData(event.getThread()); + if (data.getSamples() == null) { + data.setSamples(new HashMap<>()); + } + + data.getSamples().compute(stackTrace, (k, tmp) -> tmp == null ? 1 : tmp + 1); + data.count += 1; + } + + public List buildTaskCounts() { + List counts = new ArrayList<>(); + for (TaskCountData data : this.data.values()) { + if (data.count == 0) { + continue; + } + + TaskCount ts = new TaskCount(); + Task ta = new Task(); + ta.setId(data.getThread().getJavaThreadId()); + ta.setName(context.getThread(data.getThread()).getName()); + ts.setTask(ta); + + if (data.getSamples() != null) { + ts.setCount(data.count); + ts.setSamples(data.getSamples().entrySet().stream().collect( + Collectors.toMap( + e -> StackTraceUtil.build(e.getKey(), context.getSymbols()), + Map.Entry::getValue, + Long::sum) + )); + } + + counts.add(ts); + } + + counts.sort((o1, o2) -> { + long delta = o2.getCount() - o1.getCount(); + return delta > 0 ? 1 : (delta == 0 ? 0 : -1); + }); + + return counts; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/EventVisitor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/EventVisitor.java new file mode 100644 index 00000000..a5b8e187 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/EventVisitor.java @@ -0,0 +1,109 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; + +public abstract class EventVisitor { + void visitUnsignedIntFlag(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitGarbageCollection(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitCPUInformation(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitEnvVar(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitCPCRuntimeInformation(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitActiveSetting(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitThreadStart(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitProcessCPULoad(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitThreadCPULoad(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitExecutionSample(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitNativeExecutionSample(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitExecuteVMOperation(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitObjectAllocationInNewTLAB(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitObjectAllocationOutsideTLAB(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitFileRead(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitFileWrite(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitFileForce(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitSocketRead(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitSocketWrite(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitMonitorEnter(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitThreadPark(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitClassLoad(RecordedEvent event) { + throw new UnsupportedOperationException(); + } + + void visitThreadSleep(RecordedEvent event) { + throw new UnsupportedOperationException(); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/Extractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/Extractor.java new file mode 100644 index 00000000..a7f89bab --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/Extractor.java @@ -0,0 +1,78 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.AnalysisResult; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +public abstract class Extractor extends EventVisitor { + private static final Map> DISPATCHER = new HashMap<>() { + { + put(EventConstant.GARBAGE_COLLECTION, EventVisitor::visitGarbageCollection); + put(EventConstant.UNSIGNED_INT_FLAG, EventVisitor::visitUnsignedIntFlag); + put(EventConstant.CPU_INFORMATION, EventVisitor::visitCPUInformation); + put(EventConstant.CPC_RUNTIME_INFORMATION, EventVisitor::visitCPCRuntimeInformation); + put(EventConstant.ENV_VAR, EventVisitor::visitEnvVar); + put(EventConstant.ACTIVE_SETTING, EventVisitor::visitActiveSetting); + put(EventConstant.THREAD_START, EventVisitor::visitThreadStart); + put(EventConstant.THREAD_CPU_LOAD, EventVisitor::visitThreadCPULoad); + put(EventConstant.PROCESS_CPU_LOAD, EventVisitor::visitProcessCPULoad); + put(EventConstant.EXECUTION_SAMPLE, EventVisitor::visitExecutionSample); + put(EventConstant.NATIVE_EXECUTION_SAMPLE, EventVisitor::visitNativeExecutionSample); + put(EventConstant.EXECUTE_VM_OPERATION, EventVisitor::visitExecuteVMOperation); + put(EventConstant.OBJECT_ALLOCATION_IN_NEW_TLAB, EventVisitor::visitObjectAllocationInNewTLAB); + put(EventConstant.OBJECT_ALLOCATION_OUTSIDE_TLAB, EventVisitor::visitObjectAllocationOutsideTLAB); + + put(EventConstant.FILE_FORCE, EventVisitor::visitFileForce); + put(EventConstant.FILE_READ, EventVisitor::visitFileRead); + put(EventConstant.FILE_WRITE, EventVisitor::visitFileWrite); + + put(EventConstant.SOCKET_READ, EventVisitor::visitSocketRead); + put(EventConstant.SOCKET_WRITE, EventVisitor::visitSocketWrite); + + put(EventConstant.JAVA_MONITOR_ENTER, EventVisitor::visitMonitorEnter); + put(EventConstant.THREAD_PARK, EventVisitor::visitThreadPark); + + put(EventConstant.CLASS_LOAD, EventVisitor::visitClassLoad); + + put(EventConstant.THREAD_SLEEP, EventVisitor::visitThreadSleep); + } + }; + + final JFRAnalysisContext context; + + private final List interested; + + Extractor(JFRAnalysisContext context, List interested) { + this.context = context; + this.interested = interested; + } + + private boolean accept(RecordedEvent event) { + return interested.contains(event.getEventType().name()); + } + + public void process(RecordedEvent event) { + if (accept(event)) { + DISPATCHER.get(event.getEventType().name()).accept(this, event); + } + } + + public abstract void fillResult(AnalysisResult result); +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileIOExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileIOExtractor.java new file mode 100644 index 00000000..fb0d1b27 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileIOExtractor.java @@ -0,0 +1,21 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import java.util.List; + +public abstract class FileIOExtractor extends SumExtractor { + FileIOExtractor(JFRAnalysisContext context, List interested) { + super(context, interested); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileIOTimeExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileIOTimeExtractor.java new file mode 100644 index 00000000..3a630431 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileIOTimeExtractor.java @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class FileIOTimeExtractor extends SumExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.FILE_READ); + add(EventConstant.FILE_WRITE); + add(EventConstant.FILE_FORCE); + } + }); + + public FileIOTimeExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitFileRead(RecordedEvent event) { + visitEvent(event); + } + + @Override + void visitFileWrite(RecordedEvent event) { + visitEvent(event); + } + + @Override + void visitFileForce(RecordedEvent event) { + visitEvent(event); + } + + private void visitEvent(RecordedEvent event) { + long eventValue = event.getDurationNano(); + visitEvent(event, eventValue); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskSums()); + result.setFileIOTime(tsResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileReadExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileReadExtractor.java new file mode 100644 index 00000000..ea96ed55 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileReadExtractor.java @@ -0,0 +1,48 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class FileReadExtractor extends FileIOExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.FILE_READ); + } + }); + + public FileReadExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitFileRead(RecordedEvent event) { + long bytes = event.getLong("bytesRead"); + visitEvent(event, bytes); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskSums()); + result.setFileReadSize(tsResult); + } +} \ No newline at end of file diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileWriteExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileWriteExtractor.java new file mode 100644 index 00000000..79e5390f --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/FileWriteExtractor.java @@ -0,0 +1,48 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class FileWriteExtractor extends FileIOExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.FILE_WRITE); + } + }); + + public FileWriteExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitFileWrite(RecordedEvent event) { + long bytes = event.getLong("bytesWritten"); + visitEvent(event, bytes); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskSums()); + result.setFileWriteSize(tsResult); + } +} \ No newline at end of file diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/JFRAnalysisContext.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/JFRAnalysisContext.java new file mode 100644 index 00000000..921739e2 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/JFRAnalysisContext.java @@ -0,0 +1,81 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import lombok.Getter; +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.jfr.RecordedThread; +import org.eclipse.jifa.jfr.model.JavaThread; +import org.eclipse.jifa.jfr.request.AnalysisRequest; +import org.eclipse.jifa.jfr.model.symbol.SymbolBase; +import org.eclipse.jifa.jfr.model.symbol.SymbolTable; + +import java.util.*; + +public class JFRAnalysisContext { + private final Map eventTypeIds = new HashMap<>(); + private final Map threads = new HashMap<>(); + private final Map threadNameMap = new HashMap<>(); + @Getter + private final List events = new ArrayList<>(); + @Getter + private final SymbolTable symbols = new SymbolTable<>(); + @Getter + private final AnalysisRequest request; + @Getter + private final Set executionSampleEventTypeIds = new HashSet<>(); + + public JFRAnalysisContext(AnalysisRequest request) { + this.request = request; + } + + public synchronized Long getEventTypeId(String event) { + return eventTypeIds.get(event); + } + + public synchronized void putEventTypeId(String key, Long id) { + eventTypeIds.put(key, id); + if (EventConstant.EXECUTION_SAMPLE.equals(key)) { + executionSampleEventTypeIds.add(id); + } + } + + public synchronized boolean isExecutionSampleEventTypeId(long id) { + return executionSampleEventTypeIds.contains(id); + } + + public synchronized JavaThread getThread(RecordedThread thread) { + return threads.computeIfAbsent(thread.getJavaThreadId(), id -> { + + JavaThread javaThread = new JavaThread(); + javaThread.setId(id); + javaThread.setJavaId(thread.getJavaThreadId()); + javaThread.setOsId(thread.getOSThreadId()); + + String name = thread.getJavaName(); + if (id < 0) { + Long sequence = threadNameMap.compute(thread.getJavaName(), (k, v) -> v == null ? 0 : v + 1); + if (sequence > 0) { + name += "-" + sequence; + } + } + javaThread.setName(name); + return javaThread; + }); + } + + public synchronized void addEvent(RecordedEvent event) { + this.events.add(event); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/NativeExecutionExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/NativeExecutionExtractor.java new file mode 100644 index 00000000..bebaf517 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/NativeExecutionExtractor.java @@ -0,0 +1,88 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.util.StackTraceUtil; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.Task; +import org.eclipse.jifa.jfr.model.TaskCount; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class NativeExecutionExtractor extends CountExtractor { + + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.NATIVE_EXECUTION_SAMPLE); + } + }); + + public NativeExecutionExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitNativeExecutionSample(RecordedEvent event) { + visitEvent(event); + } + + private List buildTaskExecutionSamples() { + List nativeSamples = new ArrayList<>(); + + for (TaskCountData data : this.data.values()) { + if (data.count == 0) { + continue; + } + + TaskCount threadSamples = new TaskCount(); + Task ta = new Task(); + ta.setId(data.getThread().getJavaThreadId()); + ta.setName(data.getThread().getJavaName()); + threadSamples.setTask(ta); + + if (data.getSamples() != null) { + threadSamples.setCount(data.count); + threadSamples.setSamples(data.getSamples().entrySet().stream().collect( + Collectors.toMap( + e -> StackTraceUtil.build(e.getKey(), context.getSymbols()), + Map.Entry::getValue, + Long::sum + ) + )); + } + + nativeSamples.add(threadSamples); + } + + nativeSamples.sort((o1, o2) -> { + long delta = o2.getCount() - o1.getCount(); + return delta > 0 ? 1 : (delta == 0 ? 0 : -1); + }); + + return nativeSamples; + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult nativeResult = new DimensionResult<>(); + nativeResult.setList(buildTaskExecutionSamples()); + result.setNativeExecutionSamples(nativeResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketReadSizeExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketReadSizeExtractor.java new file mode 100644 index 00000000..581ee901 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketReadSizeExtractor.java @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SocketReadSizeExtractor extends SumExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.SOCKET_READ); + } + }); + + public SocketReadSizeExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitSocketRead(RecordedEvent event) { + visitEvent(event); + } + + private void visitEvent(RecordedEvent event) { + long eventValue = event.getLong("bytesRead"); + visitEvent(event, eventValue); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskSums()); + result.setSocketReadSize(tsResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketReadTimeExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketReadTimeExtractor.java new file mode 100644 index 00000000..1ce351d5 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketReadTimeExtractor.java @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SocketReadTimeExtractor extends SumExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.SOCKET_READ); + } + }); + + public SocketReadTimeExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitSocketRead(RecordedEvent event) { + visitEvent(event); + } + + private void visitEvent(RecordedEvent event) { + long eventValue = event.getDurationNano(); + visitEvent(event, eventValue); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskSums()); + result.setSocketReadTime(tsResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketWriteSizeExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketWriteSizeExtractor.java new file mode 100644 index 00000000..e0307127 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketWriteSizeExtractor.java @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SocketWriteSizeExtractor extends SumExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.SOCKET_WRITE); + } + }); + + public SocketWriteSizeExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitSocketWrite(RecordedEvent event) { + visitEvent(event); + } + + private void visitEvent(RecordedEvent event) { + long eventValue = event.getLong("bytesWritten"); + visitEvent(event, eventValue); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskSums()); + result.setSocketWriteSize(tsResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketWriteTimeExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketWriteTimeExtractor.java new file mode 100644 index 00000000..06892627 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SocketWriteTimeExtractor.java @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SocketWriteTimeExtractor extends SumExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.SOCKET_WRITE); + } + }); + + public SocketWriteTimeExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitSocketWrite(RecordedEvent event) { + visitEvent(event); + } + + private void visitEvent(RecordedEvent event) { + long eventValue = event.getDurationNano(); + visitEvent(event, eventValue); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskSums()); + result.setSocketWriteTime(tsResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SumExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SumExtractor.java new file mode 100644 index 00000000..35299b08 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SumExtractor.java @@ -0,0 +1,97 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.util.StackTraceUtil; +import org.eclipse.jifa.jfr.model.TaskData; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.jfr.RecordedStackTrace; +import org.eclipse.jifa.jfr.model.jfr.RecordedThread; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.jifa.jfr.model.Task; +import org.eclipse.jifa.jfr.model.TaskSum; + +public abstract class SumExtractor extends Extractor { + SumExtractor(JFRAnalysisContext context, List interested) { + super(context, interested); + } + + public static class TaskSumData extends TaskData { + TaskSumData(RecordedThread thread) { + super(thread); + } + + long sum; + } + + protected final Map data = new HashMap<>(); + + TaskSumData getTaskSumData(RecordedThread thread) { + return data.computeIfAbsent(thread.getJavaThreadId(), i -> new TaskSumData(thread)); + } + + protected void visitEvent(RecordedEvent event, long eventValue) { + RecordedStackTrace stackTrace = event.getStackTrace(); + if (stackTrace == null) { + return; + } + + TaskSumData data = getTaskSumData(event.getThread()); + if (data.getSamples() == null) { + data.setSamples(new HashMap<>()); + } + + data.getSamples().compute(stackTrace, (k, tmp) -> tmp == null ? eventValue : tmp + eventValue); + data.sum += eventValue; + } + + public List buildTaskSums() { + List sums = new ArrayList<>(); + for (TaskSumData data : this.data.values()) { + if (data.sum == 0) { + continue; + } + + TaskSum ts = new TaskSum(); + Task ta = new Task(); + ta.setId(data.getThread().getJavaThreadId()); + ta.setName(context.getThread(data.getThread()).getName()); + ts.setTask(ta); + + if (data.getSamples() != null) { + ts.setSum(data.sum); + ts.setSamples(data.getSamples().entrySet().stream().collect( + Collectors.toMap( + e -> StackTraceUtil.build(e.getKey(), context.getSymbols()), + Map.Entry::getValue, + Long::sum) + )); + } + + sums.add(ts); + } + + sums.sort((o1, o2) -> { + long delta = o2.getSum() - o1.getSum(); + return delta > 0 ? 1 : (delta == 0 ? 0 : -1); + }); + + return sums; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SynchronizationExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SynchronizationExtractor.java new file mode 100644 index 00000000..41b7ac12 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/SynchronizationExtractor.java @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SynchronizationExtractor extends SumExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.JAVA_MONITOR_ENTER); + } + }); + + public SynchronizationExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitMonitorEnter(RecordedEvent event) { + visitEvent(event, event.getDurationNano()); + } + + @Override + void visitThreadPark(RecordedEvent event) { + visitEvent(event, event.getDurationNano()); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskSums()); + result.setSynchronization(tsResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ThreadParkExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ThreadParkExtractor.java new file mode 100644 index 00000000..187b81e7 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ThreadParkExtractor.java @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ThreadParkExtractor extends SumExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.THREAD_PARK); + } + }); + + public ThreadParkExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitThreadPark(RecordedEvent event) { + visitEvent(event); + } + + private void visitEvent(RecordedEvent event) { + long eventValue = event.getDurationNano(); + visitEvent(event, eventValue); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskSums()); + result.setThreadPark(tsResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ThreadSleepTimeExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ThreadSleepTimeExtractor.java new file mode 100644 index 00000000..1bd44c54 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/ThreadSleepTimeExtractor.java @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ThreadSleepTimeExtractor extends SumExtractor { + protected static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.THREAD_SLEEP); + } + }); + + public ThreadSleepTimeExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + } + + @Override + void visitThreadSleep(RecordedEvent event) { + visitEvent(event); + } + + private void visitEvent(RecordedEvent event) { + long eventValue = event.getLong("time") * 1000 * 1000; + visitEvent(event, eventValue); + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult tsResult = new DimensionResult<>(); + tsResult.setList(buildTaskSums()); + result.setThreadSleepTime(tsResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/WallClockExtractor.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/WallClockExtractor.java new file mode 100644 index 00000000..2eea4837 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/extractor/WallClockExtractor.java @@ -0,0 +1,169 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.extractor; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jifa.jfr.common.EventConstant; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.jfr.RecordedEvent; +import org.eclipse.jifa.jfr.model.jfr.RecordedStackTrace; +import org.eclipse.jifa.jfr.model.jfr.RecordedThread; +import org.eclipse.jifa.jfr.util.StackTraceUtil; +import org.eclipse.jifa.jfr.model.DimensionResult; +import org.eclipse.jifa.jfr.model.TaskData; +import org.eclipse.jifa.jfr.model.TaskSum; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +public class WallClockExtractor extends Extractor { + private static final int ASYNC_PROFILER_DEFAULT_INTERVAL = 50 * 1000 * 1000; + + private static final List INTERESTED = Collections.unmodifiableList(new ArrayList<>() { + { + add(EventConstant.ACTIVE_SETTING); + add(EventConstant.WALL_CLOCK_SAMPLE); + } + }); + + static class TaskWallClockData extends TaskData { + private long begin = 0; + private long end = 0; + private long sampleCount = 0; + + TaskWallClockData(RecordedThread thread) { + super(thread); + } + + void updateTime(long time) { + if (begin == 0 || time < begin) { + begin = time; + } + if (end == 0 || time > end) { + end = time; + } + } + + long getDuration() { + return end - begin; + } + } + + private final Map data = new HashMap<>(); + private long methodSampleEventId = -1; + private long interval; // nano + + private boolean isWallClockEvents = false; + + public WallClockExtractor(JFRAnalysisContext context) { + super(context, INTERESTED); + + Long id = context.getEventTypeId(EventConstant.WALL_CLOCK_SAMPLE); + if (id != null) { + methodSampleEventId = context.getEventTypeId(EventConstant.WALL_CLOCK_SAMPLE); + } + } + + TaskWallClockData getThreadData(RecordedThread thread) { + return data.computeIfAbsent(thread.getJavaThreadId(), i -> new TaskWallClockData(thread)); + } + + @Override + void visitActiveSetting(RecordedEvent event) { + if (EventConstant.EVENT.equals(event.getString("name")) && EventConstant.WALL.equals(event.getString("value"))) { + this.isWallClockEvents = true; + } + + if (event.getSettingFor().getEventId() == methodSampleEventId) { + if (EventConstant.WALL.equals(event.getString("name"))) { + this.isWallClockEvents = true; + this.interval = Long.parseLong(event.getString("value")) * 1000 * 1000; + } + if (EventConstant.INTERVAL.equals(event.getString("name"))) { + this.interval = Long.parseLong(event.getString("value")) * 1000 * 1000; + } + } + } + + @Override + void visitExecutionSample(RecordedEvent event) { + RecordedStackTrace stackTrace = event.getStackTrace(); + if (stackTrace == null) { + return; + } + + RecordedThread thread = event.getThread("eventThread"); + if (thread == null) { + thread = event.getThread("sampledThread"); + } + if (thread == null) { + return; + } + TaskWallClockData taskWallClockData = getThreadData(thread); + + if (taskWallClockData.getSamples() == null) { + taskWallClockData.setSamples(new HashMap<>()); + } + taskWallClockData.updateTime(event.getStartTimeNanos()); + taskWallClockData.getSamples().compute(stackTrace, (k, count) -> count == null ? 1 : count + 1); + taskWallClockData.sampleCount++; + } + + private List buildThreadWallClock() { + List taskSumList = new ArrayList<>(); + if (!isWallClockEvents) { + return taskSumList; + } + + if (this.interval <= 0) { + this.interval = ASYNC_PROFILER_DEFAULT_INTERVAL; + log.warn("use default interval: " + ASYNC_PROFILER_DEFAULT_INTERVAL / 1000 / 1000 + " ms"); + } + Map map = new HashMap<>(); + for (TaskWallClockData data : this.data.values()) { + if (data.getSamples() == null) { + continue; + } + TaskSum taskSum = new TaskSum(); + taskSum.setTask(context.getThread(data.getThread())); + taskSum.setSum(data.sampleCount > 1 ? data.getDuration() : this.interval); + data.getSamples().replaceAll((k, v) -> v * (taskSum.getSum() / data.sampleCount)); + taskSum.setSamples(data.getSamples().entrySet().stream().collect( + Collectors.toMap( + e -> StackTraceUtil.build(e.getKey(), context.getSymbols()), + Map.Entry::getValue, + Long::sum) + )); + map.put(data.getThread().getJavaThreadId(), taskSum); + } + + map.forEach((k, v) -> { + taskSumList.add(v); + }); + + taskSumList.sort((o1, o2) -> { + long delta = o2.getSum() - o1.getSum(); + return delta > 0 ? 1 : (delta == 0 ? 0 : -1); + }); + + return taskSumList; + } + + @Override + public void fillResult(AnalysisResult result) { + DimensionResult wallClockResult = new DimensionResult<>(); + wallClockResult.setList(buildThreadWallClock()); + result.setWallClock(wallClockResult); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/AnalysisResult.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/AnalysisResult.java new file mode 100644 index 00000000..48ff82e4 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/AnalysisResult.java @@ -0,0 +1,63 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.*; + +import java.util.List; + +@Setter +@Getter +public class AnalysisResult { + private long processingTimeMillis; + + private DimensionResult cpuTime; + + private DimensionResult cpuSample; + + private DimensionResult wallClock; + + private DimensionResult allocations; + + private DimensionResult allocatedMemory; + + private DimensionResult nativeExecutionSamples; + + private DimensionResult fileIOTime; + + private DimensionResult fileReadSize; + + private DimensionResult fileWriteSize; + + private DimensionResult socketReadSize; + + private DimensionResult socketReadTime; + + private DimensionResult socketWriteSize; + + private DimensionResult socketWriteTime; + + private DimensionResult synchronization; + + private DimensionResult threadPark; + + private DimensionResult classLoadCount; + + private DimensionResult classLoadWallTime; + + private DimensionResult threadSleepTime; + + private List problems; +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Desc.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Desc.java new file mode 100644 index 00000000..e8e8fd76 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Desc.java @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; + +@Getter +public class Desc { + private final String key; + + public Desc(String key) { + this.key = key; + } + + public static Desc of(String code) { + if (code == null) { + return null; + } + return new Desc(code); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/DimensionResult.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/DimensionResult.java new file mode 100644 index 00000000..3c99e1c6 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/DimensionResult.java @@ -0,0 +1,33 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +public class DimensionResult { + + private List list; + + public void add(T t) { + if (list == null) { + list = new ArrayList<>(); + } + list.add(t); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Filter.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Filter.java new file mode 100644 index 00000000..6471ef4c --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Filter.java @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; + +@Getter +public class Filter { + private final String key; + + private final Desc desc; + + public Filter(String key, Desc desc) { + this.key = key; + this.desc = desc; + } + + public static Filter of(String key, String desc) { + return new Filter(key, Desc.of(desc)); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Frame.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Frame.java new file mode 100644 index 00000000..bb84faa0 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Frame.java @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.symbol.SymbolBase; + +import java.util.Objects; + +public class Frame extends SymbolBase { + + @Setter + @Getter + private Method method; + + @Setter + @Getter + private int line; + + private String string; + + public String toString() { + if (this.string != null) { + return string; + } + + if (this.line == 0) { + this.string = method.toString(); + } else { + this.string = String.format("%s:%d", method, line); + } + + return this.string; + } + + public int genHashCode() { + return Objects.hash(method, line); + } + + public boolean isEquals(Object b) { + if (!(b instanceof Frame f2)) { + return false; + } + + return line == f2.getLine() && method.equals(f2.getMethod()); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaFrame.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaFrame.java new file mode 100644 index 00000000..2480eabe --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaFrame.java @@ -0,0 +1,59 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.Frame; + +public class JavaFrame extends Frame { + public enum Type { + INTERPRETER("Interpreted"), + JIT("JIT compiled"), + INLINE("Inlined"), + NATIVE("Native"); + + final String value; + + Type(String value) { + this.value = value; + } + + public static Type typeOf(String value) { + for (Type type : Type.values()) { + if (type.value.equals(value)) { + return type; + } + } + throw new IllegalArgumentException(value); + } + } + + private boolean isJavaFrame; + + @Setter + @Getter + private Type type; + + @Setter + @Getter + private long bci = -1; + + public boolean isJavaFrame() { + return isJavaFrame; + } + + public void setJavaFrame(boolean javaFrame) { + isJavaFrame = javaFrame; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaMethod.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaMethod.java new file mode 100644 index 00000000..9134588d --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaMethod.java @@ -0,0 +1,48 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.Method; + +import java.util.Objects; + +@Setter +@Getter +public class JavaMethod extends Method { + private int modifiers; + private boolean hidden; + + public int genHashCode() { + return Objects.hash(modifiers, hidden, getPackageName(), getType(), getName(), getDescriptor()); + } + + public boolean equals(Object b) { + if (this == b) { + return true; + } + + if (b == null) { + return false; + } + + if (!(b instanceof JavaMethod)) { + return false; + } + + JavaMethod m2 = (JavaMethod) b; + + return modifiers == m2.modifiers && hidden == m2.hidden && super.equals(m2); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaThread.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaThread.java new file mode 100644 index 00000000..bf70b8ca --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaThread.java @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.Task; + +@Setter +@Getter +public class JavaThread extends Task { + private long javaId; + private long osId; +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaThreadCPUTime.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaThreadCPUTime.java new file mode 100644 index 00000000..f877461a --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/JavaThreadCPUTime.java @@ -0,0 +1,18 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import org.eclipse.jifa.jfr.model.TaskCPUTime; + +public class JavaThreadCPUTime extends TaskCPUTime { +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/LeafPerfDimension.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/LeafPerfDimension.java new file mode 100644 index 00000000..593da1de --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/LeafPerfDimension.java @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import org.eclipse.jifa.jfr.enums.Unit; + +@Getter +public class LeafPerfDimension { + private final String key; + + private final Desc desc; + + private final Filter[] filters; + + private final Unit unit; + + public LeafPerfDimension(String key, Desc desc, Filter[] filters, Unit unit) { + this.key = key; + this.desc = desc; + this.filters = filters; + this.unit = unit; + } + + public LeafPerfDimension(String key, Desc desc, Filter[] filters) { + this(key, desc, filters, null); + } + + public static LeafPerfDimension of(String key, String desc, Filter[] filters) { + return new LeafPerfDimension(key, Desc.of(desc), filters); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Method.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Method.java new file mode 100644 index 00000000..af441046 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Method.java @@ -0,0 +1,77 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.symbol.SymbolBase; + +import java.util.Objects; + +@Setter +@Getter +public class Method extends SymbolBase { + private String packageName; + + private String type; + + private String name; + + private String descriptor; + + private String string; + + public int genHashCode() { + return Objects.hash(packageName, type, name, descriptor); + } + + public boolean isEquals(Object b) { + if (!(b instanceof Method m2)) { + return false; + } + + return Objects.equals(packageName, m2.getPackageName()) + && Objects.equals(type, m2.getType()) + && Objects.equals(name, m2.getName()) + && Objects.equals(descriptor, m2.getDescriptor()); + } + + public String toString(boolean includeDescriptor) { + if (string != null) { + return string; + } + + String str; + if (this.descriptor != null && !this.descriptor.isEmpty() && includeDescriptor) { + str = String.format("%s%s", this.name, this.descriptor); + } else { + str = this.name; + } + + if (this.type != null && !this.type.isEmpty()) { + str = String.format("%s.%s", this.type, str); + } + + if (this.packageName != null && !this.packageName.isEmpty()) { + str = String.format("%s.%s", this.packageName, str); + } + + this.string = str; + + return str; + } + + public String toString() { + return toString(true); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/PerfDimension.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/PerfDimension.java new file mode 100644 index 00000000..cefeb69e --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/PerfDimension.java @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import org.eclipse.jifa.jfr.enums.Unit; + +@Getter +public class PerfDimension extends LeafPerfDimension { + private final LeafPerfDimension[] subDimensions; + + public PerfDimension(String key, Desc desc, Filter[] filters) { + this(key, desc, filters, Unit.COUNT); + } + + public PerfDimension(String key, Desc desc, Filter[] filters, Unit unit) { + super(key, desc, filters, unit); + this.subDimensions = null; + } + + public PerfDimension(String key, Desc desc, LeafPerfDimension[] subDimensions) { + super(key, desc, null, null); + this.subDimensions = subDimensions; + } + + public static PerfDimension of(String key, String desc, Filter[] filters) { + return new PerfDimension(key, Desc.of(desc), filters); + } + + public static PerfDimension of(String key, String desc, Filter[] filters, Unit unit) { + return new PerfDimension(key, Desc.of(desc), filters, unit); + } + + public static PerfDimension of(String key, String desc, LeafPerfDimension[] subDimensions) { + return new PerfDimension(key, Desc.of(desc), subDimensions); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Problem.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Problem.java new file mode 100644 index 00000000..2523a412 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Problem.java @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class Problem { + + private String summary; + + private String solution; + + public Problem(String summary, String solution) { + this.summary = summary; + this.solution = solution; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/StackTrace.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/StackTrace.java new file mode 100644 index 00000000..c2335988 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/StackTrace.java @@ -0,0 +1,41 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.symbol.SymbolBase; + +import java.util.Arrays; +import java.util.Objects; + +@Setter +@Getter +public class StackTrace extends SymbolBase { + + private Frame[] frames; + + private boolean truncated; + + public int genHashCode() { + return Objects.hash(truncated, Arrays.hashCode(frames)); + } + + public boolean isEquals(Object b) { + if (!(b instanceof StackTrace t2)) { + return false; + } + + return truncated == t2.truncated && Arrays.equals(frames, t2.frames); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Task.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Task.java new file mode 100644 index 00000000..e89dc83c --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/Task.java @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class Task { + + private long id; + + private String name; + + // unit: ms, -1 means unknown + private long start = -1; + + // unit: ms, -1 means unknown + private long end = -1; +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskAllocatedMemory.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskAllocatedMemory.java new file mode 100644 index 00000000..f9b1f482 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskAllocatedMemory.java @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class TaskAllocatedMemory extends TaskSum { + public TaskAllocatedMemory() { + super(null); + } + + private long allocatedMemory; +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskAllocations.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskAllocations.java new file mode 100644 index 00000000..52b68057 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskAllocations.java @@ -0,0 +1,22 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class TaskAllocations extends TaskCount { + private long allocations; +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskCPUTime.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskCPUTime.java new file mode 100644 index 00000000..97957c63 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskCPUTime.java @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class TaskCPUTime extends TaskResultBase { + + private long user; + + private long system; + + public TaskCPUTime() { + } + + public long totalCPUTime() { + return user + system; + } + + public TaskCPUTime(Task task) { + super(task); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskCount.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskCount.java new file mode 100644 index 00000000..0e9913a6 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskCount.java @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class TaskCount extends TaskResultBase { + private long count; + + public TaskCount() { + } + + public TaskCount(Task task) { + super(task); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskData.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskData.java new file mode 100644 index 00000000..76fb238a --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskData.java @@ -0,0 +1,32 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.jfr.RecordedStackTrace; +import org.eclipse.jifa.jfr.model.jfr.RecordedThread; + +import java.util.Map; + +@Setter +@Getter +public class TaskData { + public TaskData(RecordedThread thread) { + this.thread = thread; + } + + private RecordedThread thread; + + private Map samples; +} \ No newline at end of file diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskResultBase.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskResultBase.java new file mode 100644 index 00000000..423966ef --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskResultBase.java @@ -0,0 +1,44 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +@Setter +@Getter +public class TaskResultBase { + private Task task; + private Map samples; + + public TaskResultBase(Task task) { + this.task = task; + samples = new HashMap<>(); + } + + public TaskResultBase() { + } + + public void merge(StackTrace st, long value) { + if (samples == null) { + samples = new HashMap<>(); + } + if (st == null || value <= 0) { + return; + } + samples.put(st, samples.containsKey(st) ? samples.get(st) + value : value); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskSum.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskSum.java new file mode 100644 index 00000000..14a41661 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/TaskSum.java @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class TaskSum extends TaskResultBase { + public TaskSum() { + super(null); + } + + public TaskSum(Task task) { + super(task); + } + + private long sum; +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/EventType.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/EventType.java new file mode 100644 index 00000000..927aa2d6 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/EventType.java @@ -0,0 +1,16 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model.jfr; + +public record EventType(String name) { +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedClass.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedClass.java new file mode 100644 index 00000000..702de805 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedClass.java @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model.jfr; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.symbol.SymbolBase; + +import java.util.Objects; + +public class RecordedClass extends SymbolBase { + @Setter + @Getter + private String packageName; + @Setter + @Getter + private String name; + private String fullName; + + public String getFullName() { + if (fullName == null) { + fullName = packageName + "." + name; + } + return fullName; + } + + public boolean isEquals(Object b) { + if (!(b instanceof RecordedClass)) { + return false; + } + + RecordedClass c2 = (RecordedClass) b; + + return Objects.equals(packageName, c2.getPackageName()) && Objects.equals(name, c2.getName()); + } + + public int genHashCode() { + return Objects.hash(packageName, name); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedEvent.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedEvent.java new file mode 100644 index 00000000..3ecb4088 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedEvent.java @@ -0,0 +1,309 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model.jfr; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jifa.jfr.model.symbol.SymbolBase; +import org.eclipse.jifa.jfr.model.symbol.SymbolTable; + +import org.eclipse.jifa.jfr.common.EventConstant; +import org.openjdk.jmc.common.*; +import org.openjdk.jmc.common.item.*; +import org.openjdk.jmc.common.unit.*; +import org.openjdk.jmc.common.util.FormatToolkit; +import org.openjdk.jmc.common.util.LabeledIdentifier; + +import java.lang.reflect.Modifier; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Slf4j +public class RecordedEvent { + private static final long NANOS_PER_SECOND = 1000_000_000L; + + private final IItem item; + + private long startTime; + private long endTime = -1; + @Getter + private RecordedStackTrace stackTrace; + @Getter + private RecordedThread thread; + @Getter + private EventType eventType; + @Getter + private SettingFor settingFor = null; + + public static RecordedEvent newInstance(IItem item, SymbolTable symbols) { + RecordedEvent event = new RecordedEvent(item); + event.init(symbols); + return event; + } + + private RecordedEvent(IItem item) { + this.item = item; + } + + private void init(SymbolTable symbols) { + IMCThread imcThread = getValue("eventThread"); + if (imcThread == null) { + imcThread = getValue("sampledThread"); + } + + if (imcThread != null) { + thread = new RecordedThread(imcThread); + } + + Object value = getValue("startTime"); + if (value instanceof IQuantity) { + IQuantity v = (IQuantity) value; + startTime = toNanos(v, UnitLookup.EPOCH_NS); + } + + IType itemType = ItemToolkit.getItemType(item); + String itemTypeId = itemType.getIdentifier(); + + // fix for JDK Mission Control lib + if ((itemTypeId.startsWith(EventConstant.EXECUTION_SAMPLE) && !itemTypeId.equals(EventConstant.EXECUTION_SAMPLE))) { + itemTypeId = EventConstant.EXECUTION_SAMPLE; + } else if (itemTypeId.startsWith(EventConstant.OBJECT_ALLOCATION_OUTSIDE_TLAB) && !itemTypeId.equals(EventConstant.OBJECT_ALLOCATION_OUTSIDE_TLAB)) { + itemTypeId = EventConstant.OBJECT_ALLOCATION_OUTSIDE_TLAB; + } else if (itemTypeId.startsWith(EventConstant.OBJECT_ALLOCATION_IN_NEW_TLAB) && !itemTypeId.equals(EventConstant.OBJECT_ALLOCATION_IN_NEW_TLAB)) { + itemTypeId = EventConstant.OBJECT_ALLOCATION_IN_NEW_TLAB; + } + + this.eventType = new EventType(itemTypeId); + + IMCStackTrace s = getValue("stackTrace"); + if (s != null) { + List frames = s.getFrames(); + RecordedStackTrace st = new RecordedStackTrace(); + List list = new ArrayList<>(); + frames.forEach(frame -> { + IMCMethod method = frame.getMethod(); + + RecordedMethod m = new RecordedMethod(); + m.setDescriptor(method.getFormalDescriptor()); + m.setModifiers(method.getModifier() == null ? 0 : method.getModifier()); + + IMCType type = method.getType(); + RecordedClass c = new RecordedClass(); + c.setName(type.getTypeName()); + c.setPackageName(type.getPackage().getName()); + if (symbols.isContains(c)) { + c = (RecordedClass) symbols.get(c); + } else { + symbols.put(c); + } + m.setType(c); + m.setName(method.getMethodName()); + if (symbols.isContains(m)) { + m = (RecordedMethod) symbols.get(m); + } else { + symbols.put(m); + } + + RecordedFrame f = new RecordedFrame(); + f.setMethod(m); + f.setBytecodeIndex(frame.getBCI()); + f.setType(frame.getType().getName()); + + if (symbols.isContains(f)) { + f = (RecordedFrame) symbols.get(f); + } else { + symbols.put(f); + } + + list.add(f); + }); + st.setFrames(list); + if (symbols.isContains(st)) { + st = (RecordedStackTrace) symbols.get(st); + } else { + symbols.put(st); + } + stackTrace = st; + } + + if ("jdk.ActiveSetting".equals(itemType.getIdentifier())) { + for (Map.Entry, ? extends IDescribable> entry : itemType.getAccessorKeys().entrySet()) { + IMemberAccessor accessor = itemType.getAccessor(entry.getKey()); + if (entry.getKey().getIdentifier().equals("settingFor")) { + LabeledIdentifier id = (LabeledIdentifier) accessor.getMember(item); + this.settingFor = new SettingFor(id.getInterfaceId(), id.getImplementationId()); + break; + } + } + } + } + + @SuppressWarnings("unchecked") + public final T getValue(String name) { + IType itemType = ItemToolkit.getItemType(item); + for (Map.Entry, ? extends IDescribable> entry : itemType.getAccessorKeys().entrySet()) { + IMemberAccessor accessor = itemType.getAccessor(entry.getKey()); + if (entry.getKey().getIdentifier().equals(name)) { + return (T) accessor.getMember(item); + } + } + return null; + } + + public Duration getDuration() { + return Duration.ofNanos(getDurationNano()); + } + + public long getDurationNano() { + return getEndTimeNanos() - startTime; + } + + public String getString(String name) { + return getValue(name); + } + + public int getInt(String name) { + Number n = getValue(name); + if (n != null) { + return n.intValue(); + } else { + return 0; + } + } + + public float getFloat(String name) { + Number n = getValue(name); + if (n != null) { + return n.floatValue(); + } else { + return 0; + } + } + + public long getLong(String name) { + Number n = getValue(name); + if (n != null) { + return n.longValue(); + } else { + return 0; + } + } + + public RecordedThread getThread(String key) { + IMCThread imcThread = getValue(key); + return imcThread == null ? null : new RecordedThread(imcThread); + } + + public Instant getStartTime() { + return Instant.ofEpochSecond(startTime / NANOS_PER_SECOND, startTime % NANOS_PER_SECOND); + } + + public Instant getEndTime() { + long endTime = getEndTimeNanos(); + return Instant.ofEpochSecond(endTime / NANOS_PER_SECOND, endTime % NANOS_PER_SECOND); + } + + public long getStartTimeNanos() { + return startTime; + } + + private long getEndTimeNanos() { + if (endTime < 0) { + Object value = getValue("duration"); + if (value instanceof IQuantity) { + endTime = startTime + toNanos((IQuantity) value, UnitLookup.NANOSECOND); + } else { + throw new RuntimeException("should not reach here"); + } + } + + return endTime; + } + + private static long toNanos(IQuantity value, IUnit targetUnit) { + IScalarAffineTransform t = value.getUnit().valueTransformTo(targetUnit); + return t.targetValue(value.longValue()); + } + + private static String stringify(String indent, Object value) { + if (value instanceof IMCMethod) { + return indent + stringifyMethod((IMCMethod) value); + } + if (value instanceof IMCType) { + return indent + stringifyType((IMCType) value); + } + if (value instanceof IQuantity) { + return ((IQuantity) value).persistableString(); + } + + if (value instanceof IDescribable) { + String name = ((IDescribable) value).getName(); + return (name != null) ? name : value.toString(); + } + if (value == null) { + return "null"; + } + if (value.getClass().isArray()) { + StringBuilder buffer = new StringBuilder(); + Object[] values = (Object[]) value; + buffer.append(" [" + values.length + "]"); + for (Object o : values) { + buffer.append(indent); + buffer.append(stringify(indent + " ", o)); + } + return buffer.toString(); + } + return value.toString(); + } + + private static String stringifyType(IMCType type) { + return type.getPackage() == null ? + type.getTypeName() : formatPackage(type.getPackage()) + "." + type.getTypeName(); + } + + private static String stringifyMethod(IMCMethod method) { + StringBuilder buffer = new StringBuilder(); + Integer modifier = method.getModifier(); + buffer.append(formatPackage(method.getType().getPackage())); + buffer.append("."); + buffer.append(method.getType().getTypeName()); + buffer.append("#"); + buffer.append(method.getMethodName()); + buffer.append(method.getFormalDescriptor()); + buffer.append("\""); + if (modifier != null) { + buffer.append(" modifier=\""); + buffer.append(Modifier.toString(method.getModifier())); + buffer.append("\""); + } + return buffer.toString(); + } + + private static String formatPackage(IMCPackage mcPackage) { + return FormatToolkit.getPackage(mcPackage); + } + + @Getter + public static class SettingFor { + private final String eventType; + private final long eventId; + + SettingFor(String eventType, long eventId) { + this.eventId = eventId; + this.eventType = eventType; + } + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedFrame.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedFrame.java new file mode 100644 index 00000000..a78d24e3 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedFrame.java @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model.jfr; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.symbol.SymbolBase; + +import java.util.Objects; + +@Setter +@Getter +public class RecordedFrame extends SymbolBase { + private boolean javaFrame; + private String type; + private int bytecodeIndex; + private RecordedMethod method; + private int lineNumber; + + public RecordedFrame(boolean javaFrame, String type, int bytecodeIndex, int lineNumber, RecordedMethod method) { + this.javaFrame = javaFrame; + this.type = type; + this.bytecodeIndex = bytecodeIndex; + this.lineNumber = lineNumber; + this.method = method; + } + + public RecordedFrame() { + } + + public boolean isEquals(Object b) { + if (!(b instanceof RecordedFrame)) { + return false; + } + + RecordedFrame f2 = (RecordedFrame) b; + + return bytecodeIndex == f2.getBytecodeIndex() + && lineNumber == f2.getLineNumber() + && javaFrame == f2.isJavaFrame() + && this.method.equals(f2.method) + && Objects.equals(type, f2.getType()); + } + + public int genHashCode() { + return Objects.hash(javaFrame, type, bytecodeIndex, method, lineNumber); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedMethod.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedMethod.java new file mode 100644 index 00000000..45e2e113 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedMethod.java @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model.jfr; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.symbol.SymbolBase; + +import java.util.Objects; + +@Setter +@Getter +public class RecordedMethod extends SymbolBase { + private RecordedClass type; + private String name; + private String descriptor; + private int modifiers; + private boolean hidden; + + public boolean isEquals(Object b) { + if (!(b instanceof RecordedMethod)) { + return false; + } + + RecordedMethod m2 = (RecordedMethod) b; + + return Objects.equals(descriptor, m2.getDescriptor()) + && Objects.equals(name, m2.getName()) + && modifiers == m2.getModifiers() + && type.equals(m2.getType()) + && hidden == m2.isHidden(); + } + + public int genHashCode() { + return Objects.hash(type, name, descriptor, modifiers, hidden); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedStackTrace.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedStackTrace.java new file mode 100644 index 00000000..c5a18d06 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedStackTrace.java @@ -0,0 +1,53 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model.jfr; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.symbol.SymbolBase; + +import java.util.List; +import java.util.Objects; + +@Setter +@Getter +public class RecordedStackTrace extends SymbolBase { + private boolean truncated; + private List frames; + + public boolean isEquals(Object b) { + if (!(b instanceof RecordedStackTrace)) { + return false; + } + + RecordedStackTrace t2 = (RecordedStackTrace) b; + + if (truncated != t2.isTruncated()) { + return false; + } + + if (frames == null) { + return t2.getFrames() == null; + } + + if (frames.size() != t2.getFrames().size()) { + return false; + } + + return frames.equals(t2.getFrames()); + } + + public int genHashCode() { + return Objects.hash(truncated, frames); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedThread.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedThread.java new file mode 100644 index 00000000..aeb9f61b --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/jfr/RecordedThread.java @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model.jfr; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.openjdk.jmc.common.IMCThread; +import org.openjdk.jmc.common.unit.IQuantity; + +import java.lang.reflect.Field; + +@Slf4j +public class RecordedThread { + @Setter + @Getter + private long javaThreadId; + @Getter + private String javaName; + @Setter + private long osThreadId; + + public RecordedThread(String javaName, long javaThreadId, long osThreadId) { + this.javaName = javaName; + this.javaThreadId = javaThreadId; + this.osThreadId = osThreadId; + } + + public RecordedThread(IMCThread imcThread) { + this.javaThreadId = imcThread.getThreadId(); + this.javaName = imcThread.getThreadName(); + try { + Field f = imcThread.getClass().getDeclaredField("osThreadId"); + f.setAccessible(true); + Object value = f.get(imcThread); + if (value instanceof IQuantity) { + this.osThreadId = ((IQuantity) value).longValue(); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + if (this.javaThreadId == 0 && this.osThreadId > 0) { + this.javaThreadId = -this.osThreadId; + } + } + + public long getOSThreadId() { + return osThreadId; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/symbol/SymbolBase.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/symbol/SymbolBase.java new file mode 100644 index 00000000..8c37382d --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/symbol/SymbolBase.java @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model.symbol; + +public abstract class SymbolBase { + private Integer hashCode = null; + + public abstract int genHashCode(); + + public abstract boolean isEquals(Object b); + + public boolean equals(Object b) { + if (this == b) { + return true; + } + + if (b == null) { + return false; + } + + if (!(b instanceof SymbolBase)) { + return false; + } + + return isEquals(b); + } + + public int hashCode() { + if (hashCode == null) { + hashCode = genHashCode(); + } + + return hashCode; + } +} \ No newline at end of file diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/symbol/SymbolTable.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/symbol/SymbolTable.java new file mode 100644 index 00000000..a142db2f --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/model/symbol/SymbolTable.java @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.model.symbol; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SymbolTable { + private final Map table = new ConcurrentHashMap<>(); + + public boolean isContains(T s) { + return table.containsKey(s); + } + + public T get(T s) { + return table.get(s); + } + + public T put(T s) { + return table.put(s, s); + } + + public void clear() { + this.table.clear(); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/request/AnalysisRequest.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/request/AnalysisRequest.java new file mode 100644 index 00000000..c3cba011 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/request/AnalysisRequest.java @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.request; + +import lombok.Getter; + +import java.io.InputStream; +import java.nio.file.Path; + +@Getter +public class AnalysisRequest { + private final int parallelWorkers; + private final Path input; + private final InputStream inputStream; + private final int dimensions; + + public AnalysisRequest(Path input, int dimensions) { + this(1, input, dimensions); + } + + public AnalysisRequest(InputStream stream, int dimensions) { + this(1, null, stream, dimensions); + } + + public AnalysisRequest(int parallelWorkers, Path input, int dimensions) { + this(parallelWorkers, input, null, dimensions); + } + + private AnalysisRequest(int parallelWorkers, Path p, InputStream stream, int dimensions) { + this.parallelWorkers = parallelWorkers; + this.input = p; + this.dimensions = dimensions; + this.inputStream = stream; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/request/DimensionBuilder.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/request/DimensionBuilder.java new file mode 100644 index 00000000..0431f9ca --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/request/DimensionBuilder.java @@ -0,0 +1,146 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.request; + +import org.eclipse.jifa.jfr.common.ProfileDimension; + +public class DimensionBuilder { + public static final int CPU = ProfileDimension.CPU.getValue(); + public static final int CPU_SAMPLE = ProfileDimension.CPU_SAMPLE.getValue(); + public static final int WALL_CLOCK = ProfileDimension.WALL_CLOCK.getValue(); + public static final int NATIVE_EXECUTION_SAMPLES = ProfileDimension.NATIVE_EXECUTION_SAMPLES.getValue(); + public static final int ALLOC = ProfileDimension.ALLOC.getValue(); + public static final int MEM = ProfileDimension.MEM.getValue(); + public static final int FILE_IO_TIME = ProfileDimension.FILE_IO_TIME.getValue(); + public static final int FILE_WRITE_SIZE = ProfileDimension.FILE_WRITE_SIZE.getValue(); + public static final int FILE_READ_SIZE = ProfileDimension.FILE_READ_SIZE.getValue(); + public static final int SOCKET_READ_SIZE = ProfileDimension.SOCKET_READ_SIZE.getValue(); + public static final int SOCKET_READ_TIME = ProfileDimension.SOCKET_READ_TIME.getValue(); + public static final int SOCKET_WRITE_SIZE = ProfileDimension.SOCKET_WRITE_SIZE.getValue(); + public static final int SOCKET_WRITE_TIME = ProfileDimension.SOCKET_WRITE_TIME.getValue(); + public static final int SYNCHRONIZATION = ProfileDimension.SYNCHRONIZATION.getValue(); + public static final int THREAD_PARK = ProfileDimension.THREAD_PARK.getValue(); + public static final int CLASS_LOAD_COUNT = ProfileDimension.CLASS_LOAD_COUNT.getValue(); + public static final int CLASS_LOAD_WALL_TIME = ProfileDimension.CLASS_LOAD_WALL_TIME.getValue(); + public static final int THREAD_SLEEP = ProfileDimension.THREAD_SLEEP.getValue(); + + public static final int ALL = CPU | CPU_SAMPLE | WALL_CLOCK | NATIVE_EXECUTION_SAMPLES + | ALLOC | MEM | FILE_IO_TIME | FILE_WRITE_SIZE | FILE_READ_SIZE | SOCKET_READ_SIZE | SOCKET_WRITE_SIZE + | SOCKET_READ_TIME | SOCKET_WRITE_TIME | SYNCHRONIZATION | THREAD_PARK + | CLASS_LOAD_COUNT | CLASS_LOAD_WALL_TIME | THREAD_SLEEP; + + private int dimensions = 0; + + public static DimensionBuilder newInstance() { + return new DimensionBuilder(); + } + + public DimensionBuilder enableCPU() { + this.dimensions |= CPU; + return this; + } + + public DimensionBuilder enableCPUSample() { + this.dimensions |= CPU_SAMPLE; + return this; + } + + public DimensionBuilder enableWallClock() { + this.dimensions |= WALL_CLOCK; + return this; + } + + public DimensionBuilder enableNative() { + this.dimensions |= NATIVE_EXECUTION_SAMPLES; + return this; + } + + public DimensionBuilder enableAllocCount() { + this.dimensions |= ALLOC; + return this; + } + + public DimensionBuilder enableAllocSize() { + this.dimensions |= MEM; + return this; + } + + public DimensionBuilder enableFileIOTime() { + this.dimensions |= FILE_IO_TIME; + return this; + } + + public DimensionBuilder enableFileWriteSize() { + this.dimensions |= FILE_WRITE_SIZE; + return this; + } + + public DimensionBuilder enableFileReadSize() { + this.dimensions |= FILE_READ_SIZE; + return this; + } + + public DimensionBuilder enableSocketReadTime() { + this.dimensions |= SOCKET_READ_TIME; + return this; + } + + public DimensionBuilder enableSocketReadSize() { + this.dimensions |= SOCKET_READ_SIZE; + return this; + } + + public DimensionBuilder enableSocketWriteSize() { + this.dimensions |= SOCKET_WRITE_SIZE; + return this; + } + + public DimensionBuilder enableSocketWriteTime() { + this.dimensions |= SOCKET_WRITE_TIME; + return this; + } + + public DimensionBuilder enableThreadPark() { + this.dimensions |= THREAD_PARK; + return this; + } + + public DimensionBuilder enableSynchronization() { + this.dimensions |= SYNCHRONIZATION; + return this; + } + + public DimensionBuilder enableClassLoadTime() { + this.dimensions |= CLASS_LOAD_WALL_TIME; + return this; + } + + public DimensionBuilder enableClassLoadCount() { + this.dimensions |= CLASS_LOAD_COUNT; + return this; + } + + public DimensionBuilder enableThreadSleep() { + this.dimensions |= THREAD_SLEEP; + return this; + } + + public DimensionBuilder enableALL() { + this.dimensions = ALL; + return this; + } + + public int build() { + return this.dimensions; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/DescriptorUtil.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/DescriptorUtil.java new file mode 100644 index 00000000..4d2aa906 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/DescriptorUtil.java @@ -0,0 +1,51 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.util; + +import org.objectweb.asm.Type; + +import java.util.HashMap; +import java.util.Map; + +public class DescriptorUtil { + private final Map CACHE = new HashMap<>(); + + public String decodeMethodArgs(String descriptor) { + if (descriptor == null || descriptor.isEmpty()) { + return ""; + } + + if (CACHE.containsKey(descriptor)) { + return CACHE.get(descriptor); + } + + Type methodType = Type.getMethodType(descriptor); + StringBuilder b = new StringBuilder("("); + Type[] argTypes = methodType.getArgumentTypes(); + for (int ix = 0; ix < argTypes.length; ix++) { + if (ix != 0) { + b.append(", "); + } + b.append(trimPackage(argTypes[ix].getClassName())); + } + b.append(')'); + String str = b.toString(); + CACHE.put(descriptor, str); + + return str; + } + + private static String trimPackage(String className) { + return className.contains(".") ? className.substring(className.lastIndexOf(".") + 1) : className; + } +} \ No newline at end of file diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/GCUtil.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/GCUtil.java new file mode 100644 index 00000000..a58b4e24 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/GCUtil.java @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.util; + +import java.util.ArrayList; +import java.util.List; + +public class GCUtil { + // JDK 8 default: ParallelScavenge + ParallelOld + // CMS: ParNew + ConcurrentMarkSweep + SerialOld + // G1: G1New + G1Old + SerialOld + + private static final List PARALLEL_GC = new ArrayList() {{ + add("G1New"); + add("ParNew"); + add("ParallelScavenge"); + add("ParallelOld"); + }}; + + private static final List CONCURRENT_GC = new ArrayList() {{ + add("G1Old"); + add("ConcurrentMarkSweep"); + }}; + + private static final List SERIAL_GC = new ArrayList() {{ + add("SerialOld"); + }}; + + public static boolean isConcGC(String name) { + return CONCURRENT_GC.contains(name); + } + + public static boolean isParallelGC(String name) { + return PARALLEL_GC.contains(name); + } + + public static boolean isSerialGC(String name) { + return SERIAL_GC.contains(name); + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/StackTraceUtil.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/StackTraceUtil.java new file mode 100644 index 00000000..55349d3d --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/StackTraceUtil.java @@ -0,0 +1,83 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.util; + +import org.eclipse.jifa.jfr.model.jfr.RecordedFrame; +import org.eclipse.jifa.jfr.model.jfr.RecordedMethod; +import org.eclipse.jifa.jfr.model.jfr.RecordedStackTrace; +import org.eclipse.jifa.jfr.model.JavaFrame; +import org.eclipse.jifa.jfr.model.JavaMethod; +import org.eclipse.jifa.jfr.model.symbol.SymbolBase; +import org.eclipse.jifa.jfr.model.symbol.SymbolTable; +import org.eclipse.jifa.jfr.model.Frame; +import org.eclipse.jifa.jfr.model.StackTrace; + +import java.util.List; + +public class StackTraceUtil { + // FIXME: need cache + public static StackTrace build(RecordedStackTrace stackTrace, SymbolTable symbols) { + StackTrace result = new StackTrace(); + result.setTruncated(stackTrace.isTruncated()); + + DescriptorUtil util = new DescriptorUtil(); + List srcFrames = stackTrace.getFrames(); + Frame[] dstFrames = new Frame[srcFrames.size()]; + for (int i = 0; i < srcFrames.size(); i++) { + RecordedFrame frame = srcFrames.get(i); + Frame dstFrame; + if (frame.isJavaFrame()) { + dstFrame = new JavaFrame(); + ((JavaFrame) dstFrame).setJavaFrame(frame.isJavaFrame()); + ((JavaFrame) dstFrame).setType(JavaFrame.Type.typeOf(frame.getType())); + ((JavaFrame) dstFrame).setBci(frame.getBytecodeIndex()); + } else { + dstFrame = new Frame(); + } + + RecordedMethod method = frame.getMethod(); + JavaMethod dstMethod = new JavaMethod(); + dstMethod.setPackageName(method.getType().getPackageName()); + dstMethod.setType(method.getType().getName()); + dstMethod.setName(method.getName()); + dstMethod.setDescriptor(util.decodeMethodArgs(method.getDescriptor())); + + dstMethod.setModifiers(method.getModifiers()); + dstMethod.setHidden(method.isHidden()); + if (symbols.isContains(dstMethod)) { + dstMethod = (JavaMethod) symbols.get(dstMethod); + } else { + symbols.put(dstMethod); + } + + dstFrame.setMethod(dstMethod); + dstFrame.setLine(frame.getLineNumber()); + if (symbols.isContains(dstFrame)) { + dstFrame = (Frame) symbols.get(dstFrame); + } else { + symbols.put(dstFrame); + } + + dstFrames[i] = dstFrame; + } + + result.setFrames(dstFrames); + if (symbols.isContains(result)) { + result = (StackTrace) symbols.get(result); + } else { + symbols.put(result); + } + + return result; + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/TimeUtil.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/TimeUtil.java new file mode 100644 index 00000000..8ed3352e --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/util/TimeUtil.java @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.util; + +import static java.util.concurrent.TimeUnit.*; + +public class TimeUtil { + public static long parseTimespan(String s) { + long timeSpan = Long.parseLong(s.substring(0, s.length() - 2).trim()); + if (s.endsWith("ns")) { + return timeSpan; + } + if (s.endsWith("us")) { + return NANOSECONDS.convert(timeSpan, MICROSECONDS); + } + if (s.endsWith("ms")) { + return NANOSECONDS.convert(timeSpan, MILLISECONDS); + } + timeSpan = Long.parseLong(s.substring(0, s.length() - 1).trim()); + if (s.endsWith("s")) { + return NANOSECONDS.convert(timeSpan, SECONDS); + } + if (s.endsWith("m")) { + return 60 * NANOSECONDS.convert(timeSpan, SECONDS); + } + if (s.endsWith("h")) { + return 60 * 60 * NANOSECONDS.convert(timeSpan, SECONDS); + } + if (s.endsWith("d")) { + return 24 * 60 * 60 * NANOSECONDS.convert(timeSpan, SECONDS); + } + + try { + return Long.parseLong(s); + } catch (NumberFormatException nfe) { + // Only accept values with units + throw new NumberFormatException("Timespan '" + s + "' is invalid. Valid units are ns, us, s, m, h and d."); + } + } +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/vo/FlameGraph.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/vo/FlameGraph.java new file mode 100644 index 00000000..4e3dd813 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/vo/FlameGraph.java @@ -0,0 +1,22 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.vo; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class FlameGraph extends GraphBase { + private Object[][] data = new Object[0][]; +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/vo/GraphBase.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/vo/GraphBase.java new file mode 100644 index 00000000..bdcb5eb4 --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/vo/GraphBase.java @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.vo; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Setter +@Getter +public class GraphBase { + private List threads = new ArrayList<>(); + private Map threadSplit = new HashMap<>(); + private Map symbolTable = new HashMap<>(); +} diff --git a/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/vo/Metadata.java b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/vo/Metadata.java new file mode 100644 index 00000000..d620bcac --- /dev/null +++ b/analysis/jfr/src/main/java/org/eclipse/jifa/jfr/vo/Metadata.java @@ -0,0 +1,23 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.vo; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.jifa.jfr.model.PerfDimension; + +@Setter +@Getter +public class Metadata { + private PerfDimension[] perfDimensions; +} diff --git a/analysis/jfr/src/main/resources/META-INF/services/org.eclipse.jifa.analysis.ApiExecutor b/analysis/jfr/src/main/resources/META-INF/services/org.eclipse.jifa.analysis.ApiExecutor new file mode 100644 index 00000000..2a9d8b06 --- /dev/null +++ b/analysis/jfr/src/main/resources/META-INF/services/org.eclipse.jifa.analysis.ApiExecutor @@ -0,0 +1 @@ +org.eclipse.jifa.jfr.JFRAnalysisApiExecutor \ No newline at end of file diff --git a/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/TestJFRAnalysisApiExecutor.java b/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/TestJFRAnalysisApiExecutor.java new file mode 100644 index 00000000..e59f897f --- /dev/null +++ b/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/TestJFRAnalysisApiExecutor.java @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@Slf4j +public class TestJFRAnalysisApiExecutor { + @Test + public void test() { + JFRAnalysisApiExecutor executor = new JFRAnalysisApiExecutor(); + Assertions.assertEquals("jfr-file", executor.namespace()); + } +} diff --git a/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/TestJFRAnalyzer.java b/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/TestJFRAnalyzer.java new file mode 100644 index 00000000..7b317b57 --- /dev/null +++ b/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/TestJFRAnalyzer.java @@ -0,0 +1,587 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.tuple.Triple; +import org.eclipse.jifa.analysis.listener.ProgressListener; +import org.eclipse.jifa.jfr.api.JFRAnalyzer; +import org.eclipse.jifa.jfr.common.ProfileDimension; +import org.eclipse.jifa.jfr.model.AnalysisResult; +import org.eclipse.jifa.jfr.model.JavaThreadCPUTime; +import org.eclipse.jifa.jfr.request.DimensionBuilder; +import org.eclipse.jifa.jfr.helper.SimpleFlameGraph; +import org.eclipse.jifa.jfr.model.*; +import org.eclipse.jifa.jfr.vo.Metadata; +import org.eclipse.jifa.jfr.vo.FlameGraph; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public class TestJFRAnalyzer { + + @Test + public void testMetadata() throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException { + Path path = createTmpFileForResource("jfr.jfr"); + Method buildAnalyzer = JFRAnalysisApiExecutor.class.getDeclaredMethod("buildAnalyzer", Path.class, Map.class, ProgressListener.class); + buildAnalyzer.setAccessible(true); + JFRAnalyzer analyzer = (JFRAnalyzer) buildAnalyzer.invoke(new JFRAnalysisApiExecutor(), path, null, ProgressListener.NoOpProgressListener); + Metadata meta = analyzer.metadata(); + Assertions.assertNotNull(meta.getPerfDimensions()); + Assertions.assertTrue(meta.getPerfDimensions().length > 0); + } + + @Test + public void testFlameGraph() throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException { + Path path = createTmpFileForResource("jfr.jfr"); + Method buildAnalyzer = JFRAnalysisApiExecutor.class.getDeclaredMethod("buildAnalyzer", Path.class, Map.class, ProgressListener.class); + buildAnalyzer.setAccessible(true); + JFRAnalyzer analyzer = (JFRAnalyzer) buildAnalyzer.invoke(new JFRAnalysisApiExecutor(), path, null, ProgressListener.NoOpProgressListener); + FlameGraph fg = analyzer.getFlameGraph(ProfileDimension.CPU.getKey(), false, null); + Assertions.assertNotNull(fg.getData()); + Assertions.assertNotNull(fg.getSymbolTable()); + Assertions.assertNotNull(fg.getThreadSplit()); + } + + @Test + public void testCpu() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.CPU, null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getCpuTime()); + + List cpuTimes = result.getCpuTime().getList(); + Optional optional = cpuTimes.stream() + .filter(item -> item.getTask().getName().equals("Thread-6")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskCPUTime tct = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse((JavaThreadCPUTime) tct); + + Assertions.assertEquals(18852L, nanoToMillis(g.totalSampleValue.longValue())); + + List> list = g.queryLeafNodes(10); + Assertions.assertEquals(1, list.size()); + + optional = cpuTimes.stream().filter(item -> item.getTask().getName().equals("main")).findAny(); + Assertions.assertTrue(optional.isPresent()); + tct = optional.get(); + g = SimpleFlameGraph.parse((JavaThreadCPUTime) tct); + list = g.queryLeafNodes(10); + Assertions.assertEquals(1, list.size()); + Optional> t = list.stream().filter( + item -> item.getLeft().contains("Test.()")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(1260, nanoToMillis(Long.parseLong(t.get().getMiddle()))); + Assertions.assertEquals("42.0", t.get().getRight()); + + optional = cpuTimes.stream().filter(item -> item.getTask().getName().equals("GC Thread")).findAny(); + Assertions.assertTrue(optional.isPresent()); + tct = optional.get(); + g = SimpleFlameGraph.parse((JavaThreadCPUTime) tct); + list = g.queryLeafNodes(0); + Assertions.assertEquals(1, list.size()); + t = list.stream().filter(item -> item.getLeft().contains("JVM.GC")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(402, nanoToMillis(Long.parseLong(t.get().getMiddle()))); + } + + @Test + public void testCpu2() throws IOException { + Path path = createTmpFileForResource("ap-cpu-default.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.CPU, null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getCpuTime()); + + List cpuTimes = result.getCpuTime().getList(); + Optional optional = cpuTimes.stream() + .filter(item -> item.getTask().getName().equals("main")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskCPUTime tct = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse((JavaThreadCPUTime) tct); + + Assertions.assertEquals(9860L, nanoToMillis(g.totalSampleValue.longValue())); + + List> list = g.queryLeafNodes(10); + Assertions.assertEquals(1, list.size()); + + optional = cpuTimes.stream().filter(item -> item.getTask().getName().equals("GC task thread#-9")).findAny(); + Assertions.assertTrue(optional.isPresent()); + tct = optional.get(); + g = SimpleFlameGraph.parse((JavaThreadCPUTime) tct); + Assertions.assertEquals(30L, nanoToMillis(g.totalSampleValue.longValue())); + } + + @Test + public void testCpu3() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.CPU_SAMPLE, null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getCpuSample()); + + List cpuTimes = result.getCpuSample().getList(); + Optional optional = cpuTimes.stream() + .filter(item -> item.getTask().getName().equals("Thread-6")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskCount tct = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse(tct); + + Assertions.assertEquals(952, g.totalSampleValue.longValue()); + + List> list = g.queryLeafNodes(10); + Assertions.assertEquals(1, list.size()); + Optional> t = list.stream().filter( + item -> item.getLeft().contains("Test.realEatCpu()")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(952, Long.parseLong(t.get().getMiddle())); + } + + @Test + public void testWall() throws IOException { + Path path = createTmpFileForResource("ap-wall-default.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.WALL_CLOCK, null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertFalse(result.getWallClock().getList().isEmpty()); + + List cpuTimes = result.getWallClock().getList(); + Optional optional = cpuTimes.stream() + .filter(item -> item.getTask().getName().equals("SleepThread")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskSum tct = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse(tct); + + Assertions.assertEquals(9949, nanoToMillis(g.totalSampleValue.longValue())); + + List> list = g.queryLeafNodes(10); + Assertions.assertEquals(1, list.size()); + Optional> t = list.stream().filter( + item -> item.getLeft().contains("libpthread-2.17.so.__pthread_cond_timedwait()")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(9800, nanoToMillis(Long.parseLong(t.get().getMiddle()))); + } + + @Test + public void testNative() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.NATIVE_EXECUTION_SAMPLES, null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getNativeExecutionSamples()); + + List nativeSamples = result.getNativeExecutionSamples().getList(); + Optional optional = + nativeSamples.stream().filter(item -> item.getTask().getName().equals("Thread-2")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskCount ta = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(1001, g.totalSampleValue.intValue()); + + List> list = g.queryLeafNodes(10); + Assertions.assertEquals(1, list.size()); + + Optional> t = + list.stream().filter(item -> item.getLeft().equals("java.net.SocketInputStream.socketRead0(FileDescriptor, byte[], int, int, int)")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(951, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("95.0", t.get().getRight()); + } + + @Test + public void testAllocations() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.ALLOC, null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getAllocations()); + + List taskAllocations = result.getAllocations().getList(); + Optional optional = + taskAllocations.stream().filter(item -> item.getTask().getName().equals("main")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskAllocations ta = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(13674, g.totalSampleValue.intValue()); + + List> list = g.queryLeafNodes(10); + Assertions.assertEquals(list.size(), 1); + + Optional> t = + list.stream().filter(item -> item.getLeft().contains("Test.()")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(13663, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("99.92", t.get().getRight()); + } + + @Test + public void testAllocatedMemory() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.MEM, null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getAllocatedMemory()); + + List taskAllocations = result.getAllocatedMemory().getList(); + Optional optional = + taskAllocations.stream().filter(item -> item.getTask().getName().equals("main")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskAllocatedMemory ta = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(28609059912L, g.totalSampleValue.longValue()); + + List> list = g.queryLeafNodes(10); + Assertions.assertEquals(list.size(), 1); + + Optional> t = + list.stream().filter(item -> item.getLeft().contains("Test.()")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(28602671000L, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("99.98", t.get().getRight()); + } + + @Test + public void testCpuAndAllocAndMem() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.CPU | DimensionBuilder.ALLOC | DimensionBuilder.MEM, null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getCpuTime()); + Assertions.assertFalse(result.getCpuTime().getList().isEmpty()); + Assertions.assertNotNull(result.getAllocations()); + Assertions.assertFalse(result.getAllocations().getList().isEmpty()); + Assertions.assertNotNull(result.getAllocatedMemory()); + Assertions.assertFalse(result.getAllocatedMemory().getList().isEmpty()); + } + + @Test + public void testFileIO() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.FILE_IO_TIME | DimensionBuilder.FILE_READ_SIZE | DimensionBuilder.FILE_WRITE_SIZE, null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getFileIOTime()); + Assertions.assertFalse(result.getFileIOTime().getList().isEmpty()); + + List taskSumList = result.getFileIOTime().getList(); + Optional optional = + taskSumList.stream().filter(item -> item.getTask().getName().equals("Thread-2")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskSum ta = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(123070, g.totalSampleValue.longValue()); + List> list = g.queryLeafNodes(10); + Assertions.assertEquals(2, list.size()); + Optional> t = + list.stream().filter(item -> item.getLeft().contains("java.io.FileOutputStream.write(byte[], int, int)")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(58214, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("47.3", t.get().getRight()); + + Assertions.assertNotNull(result.getFileReadSize()); + Assertions.assertFalse(result.getFileReadSize().getList().isEmpty()); + taskSumList = result.getFileReadSize().getList(); + optional = taskSumList.stream().filter(item -> item.getTask().getName().equals("Thread-3")).findAny(); + Assertions.assertTrue(optional.isPresent()); + ta = optional.get(); + g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(6651, g.totalSampleValue.longValue()); + list = g.queryLeafNodes(20); + Assertions.assertEquals(1, list.size()); + t = list.stream().filter(item -> item.getLeft().contains("java.io.FileInputStream.read(byte[], int, int)")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(5352, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("80.47", t.get().getRight()); + + Assertions.assertNotNull(result.getFileWriteSize()); + Assertions.assertFalse(result.getFileWriteSize().getList().isEmpty()); + taskSumList = result.getFileWriteSize().getList(); + optional = taskSumList.stream().filter(item -> item.getTask().getName().equals("Thread-2")).findAny(); + Assertions.assertTrue(optional.isPresent()); + ta = optional.get(); + g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(31, g.totalSampleValue.longValue()); + list = g.queryLeafNodes(20); + Assertions.assertEquals(1, list.size()); + t = list.stream().filter(item -> item.getLeft().contains("java.io.FileOutputStream.write(byte[], int, int)")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(29, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("93.55", t.get().getRight()); + } + + @Test + public void testSocketIO() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, + DimensionBuilder.SOCKET_WRITE_SIZE | DimensionBuilder.SOCKET_WRITE_TIME + | DimensionBuilder.SOCKET_READ_SIZE | DimensionBuilder.SOCKET_READ_TIME, + null, ProgressListener.NoOpProgressListener); + + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getSocketWriteSize()); + Assertions.assertFalse(result.getSocketWriteSize().getList().isEmpty()); + List taskSumList = result.getSocketWriteSize().getList(); + Optional optional = + taskSumList.stream().filter(item -> item.getTask().getName().equals("Thread-3")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskSum ta = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(114, g.totalSampleValue.longValue()); + List> list = g.queryLeafNodes(10); + Assertions.assertEquals(1, list.size()); + Optional> t = + list.stream().filter(item -> item.getLeft().contains("java.net.SocketOutputStream.socketWrite(byte[], int, int)")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(114, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("100.0", t.get().getRight()); + + Assertions.assertNotNull(result.getSocketWriteTime()); + Assertions.assertFalse(result.getSocketWriteTime().getList().isEmpty()); + taskSumList = result.getSocketWriteTime().getList(); + optional = taskSumList.stream().filter(item -> item.getTask().getName().equals("Thread-3")).findAny(); + Assertions.assertTrue(optional.isPresent()); + ta = optional.get(); + g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(685179, g.totalSampleValue.longValue()); + list = g.queryLeafNodes(20); + Assertions.assertEquals(1, list.size()); + t = list.stream().filter(item -> item.getLeft().contains("java.net.SocketOutputStream.socketWrite(byte[], int, int)")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(685179, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("100.0", t.get().getRight()); + + Assertions.assertNotNull(result.getSocketReadTime()); + Assertions.assertFalse(result.getSocketReadTime().getList().isEmpty()); + taskSumList = result.getSocketReadTime().getList(); + optional = taskSumList.stream().filter(item -> item.getTask().getName().equals("Thread-2")).findAny(); + Assertions.assertTrue(optional.isPresent()); + ta = optional.get(); + g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(19001, nanoToMillis(g.totalSampleValue.longValue())); + list = g.queryLeafNodes(20); + Assertions.assertEquals(1, list.size()); + t = list.stream().filter(item -> item.getLeft().contains("java.net.SocketInputStream.read(byte[], int, int, int)")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(19001, nanoToMillis(Long.valueOf(t.get().getMiddle()))); + Assertions.assertEquals("100.0", t.get().getRight()); + + Assertions.assertNotNull(result.getSocketReadSize()); + Assertions.assertFalse(result.getSocketReadSize().getList().isEmpty()); + taskSumList = result.getSocketReadSize().getList(); + optional = taskSumList.stream().filter(item -> item.getTask().getName().equals("Thread-2")).findAny(); + Assertions.assertTrue(optional.isPresent()); + ta = optional.get(); + g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(114, g.totalSampleValue.longValue()); + list = g.queryLeafNodes(20); + Assertions.assertEquals(1, list.size()); + t = list.stream().filter(item -> item.getLeft().contains("java.net.SocketInputStream.read(byte[], int, int, int)")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(114, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("100.0", t.get().getRight()); + } + + @Test + public void testSynchronization() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.SYNCHRONIZATION, + null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getSynchronization()); + Assertions.assertFalse(result.getSynchronization().getList().isEmpty()); + List taskSumList = result.getSynchronization().getList(); + Optional optional = + taskSumList.stream().filter(item -> item.getTask().getName().equals("Thread-2")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskSum ta = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(33093, g.totalSampleValue.longValue()); + List> list = g.queryLeafNodes(10); + Assertions.assertEquals(1, list.size()); + Optional> t = + list.stream().filter(item -> item.getLeft().contains("java.io.PrintStream.println(String)")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(33093, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("100.0", t.get().getRight()); + } + + @Test + public void testThreadPark() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.THREAD_PARK, + null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getThreadPark()); + Assertions.assertFalse(result.getThreadPark().getList().isEmpty()); + List taskSumList = result.getThreadPark().getList(); + Optional optional = taskSumList.stream().filter(item -> item.getTask().getName().equals("Thread-5")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskSum ta = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(999, nanoToMillis(g.totalSampleValue.longValue())); + List> list = g.queryLeafNodes(20); + Assertions.assertEquals(1, list.size()); + Optional> t = list.stream().filter(item -> item.getLeft().contains("sun.misc.Unsafe.park(boolean, long)")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(999, nanoToMillis(Long.valueOf(t.get().getMiddle()))); + Assertions.assertEquals("100.0", t.get().getRight()); + } + + @Test + public void testClassLoad() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, + DimensionBuilder.CLASS_LOAD_COUNT | DimensionBuilder.CLASS_LOAD_WALL_TIME, + null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getClassLoadWallTime()); + Assertions.assertFalse(result.getClassLoadWallTime().getList().isEmpty()); + List taskSumList = result.getClassLoadWallTime().getList(); + Optional optional = + taskSumList.stream().filter(item -> item.getTask().getName().equals("Thread-2")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskSum ta = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(2, nanoToMillis(g.totalSampleValue.longValue())); + List> list = g.queryLeafNodes(7); + Assertions.assertEquals(1, list.size()); + Optional> t = + list.stream().filter(item -> item.getLeft().contains("java.net.ServerSocket.accept()")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(222770, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("7.66", t.get().getRight()); + + Assertions.assertNotNull(result.getClassLoadCount()); + Assertions.assertFalse(result.getClassLoadCount().getList().isEmpty()); + List taskCountList = result.getClassLoadCount().getList(); + Optional optional2 = taskCountList.stream().filter(item -> item.getTask().getName().equals("Thread-2")).findAny(); + Assertions.assertTrue(optional2.isPresent()); + TaskCount ta2 = optional2.get(); + g = SimpleFlameGraph.parse(ta2); + Assertions.assertEquals(24, g.totalSampleValue.longValue()); + list = g.queryLeafNodes(8); + Assertions.assertEquals(1, list.size()); + t = list.stream().filter(item -> item.getLeft().contains("sun.net.NetHooks.()")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(2, Long.valueOf(t.get().getMiddle())); + Assertions.assertEquals("8.33", t.get().getRight()); + } + + @Test + public void testSleep() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, DimensionBuilder.THREAD_SLEEP, null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertNotNull(result.getThreadSleepTime()); + + List list = result.getThreadSleepTime().getList(); + Optional optional = list.stream().filter(item -> item.getTask().getName().equals("main")).findAny(); + Assertions.assertTrue(optional.isPresent()); + TaskSum ta = optional.get(); + SimpleFlameGraph g = SimpleFlameGraph.parse(ta); + Assertions.assertEquals(16543, nanoToMillis(g.totalSampleValue.longValue())); + + List> list2 = g.queryLeafNodes(10); + Assertions.assertEquals(list2.size(), 1); + + Optional> t = + list2.stream().filter(item -> item.getLeft().contains("java.lang.Thread.sleep(long)")).findAny(); + Assertions.assertTrue(t.isPresent()); + Assertions.assertEquals(15543, nanoToMillis(Long.valueOf(t.get().getMiddle()))); + Assertions.assertEquals("93.96", t.get().getRight()); + } + + @Test + public void tesJfrNoWall() throws IOException { + Path path = createTmpFileForResource("jfr.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, + DimensionBuilder.CPU | DimensionBuilder.CPU_SAMPLE | DimensionBuilder.WALL_CLOCK, + null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertTrue(result.getWallClock().getList().isEmpty()); + Assertions.assertFalse(result.getCpuSample().getList().isEmpty()); + Assertions.assertFalse(result.getCpuTime().getList().isEmpty()); + } + + @Test + public void testAsyncCpuNoWall() throws IOException { + // asprof -e cpu -d 10 -f ap.jfr jps + // event=cpu, interval=0 + Path path = createTmpFileForResource("ap-cpu-default.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, + DimensionBuilder.CPU | DimensionBuilder.CPU_SAMPLE | DimensionBuilder.WALL_CLOCK, + null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertTrue(result.getWallClock().getList().isEmpty()); + Assertions.assertFalse(result.getCpuTime().getList().isEmpty()); + Assertions.assertFalse(result.getCpuSample().getList().isEmpty()); + + // asprof -e cpu -i 20ms -d 10 -f ap.jfr jps + // event=cpu, interval=20 + path = createTmpFileForResource("ap-cpu-20.jfr"); + analyzer = new JFRAnalyzerImpl(path, + DimensionBuilder.CPU | DimensionBuilder.CPU_SAMPLE | DimensionBuilder.WALL_CLOCK, + null, ProgressListener.NoOpProgressListener); + result = analyzer.getResult(); + Assertions.assertTrue(result.getWallClock().getList().isEmpty()); + Assertions.assertFalse(result.getCpuTime().getList().isEmpty()); + Assertions.assertFalse(result.getCpuSample().getList().isEmpty()); + } + + @Test + public void testAsyncWallNoCpu() throws IOException { + // asprof -e wall -d 10 -f ap.jfr jps + // event=wall, interval=0 + Path path = createTmpFileForResource("ap-wall-default.jfr"); + JFRAnalyzerImpl analyzer = new JFRAnalyzerImpl(path, + DimensionBuilder.CPU | DimensionBuilder.CPU_SAMPLE | DimensionBuilder.WALL_CLOCK, + null, ProgressListener.NoOpProgressListener); + AnalysisResult result = analyzer.getResult(); + Assertions.assertFalse(result.getWallClock().getList().isEmpty()); + Assertions.assertTrue(result.getCpuTime().getList().isEmpty()); + Assertions.assertTrue(result.getCpuSample().getList().isEmpty()); + + // asprof --wall 20ms -d 10 -f ap.jfr jps + // wall=20 + path = createTmpFileForResource("ap-wall-20.jfr"); + analyzer = new JFRAnalyzerImpl(path, + DimensionBuilder.CPU | DimensionBuilder.CPU_SAMPLE | DimensionBuilder.WALL_CLOCK, + null, ProgressListener.NoOpProgressListener); + result = analyzer.getResult(); + Assertions.assertFalse(result.getWallClock().getList().isEmpty()); + Assertions.assertTrue(result.getCpuTime().getList().isEmpty()); + Assertions.assertTrue(result.getCpuSample().getList().isEmpty()); + + // asprof -e wall -i 20ms -d 10 -f ap.jfr jps + // event=wall, interval=20 + path = createTmpFileForResource("ap-wall-20-1.jfr"); + analyzer = new JFRAnalyzerImpl(path, + DimensionBuilder.CPU | DimensionBuilder.CPU_SAMPLE | DimensionBuilder.WALL_CLOCK, + null, ProgressListener.NoOpProgressListener); + result = analyzer.getResult(); + Assertions.assertFalse(result.getWallClock().getList().isEmpty()); + Assertions.assertTrue(result.getCpuTime().getList().isEmpty()); + Assertions.assertTrue(result.getCpuSample().getList().isEmpty()); + } + + public static Path createTmpFileForResource(String resource) throws IOException { + Path path = Files.createTempFile("temp", ".jfr"); + path.toFile().deleteOnExit(); + FileUtils.copyInputStreamToFile(Objects.requireNonNull( + TestJFRAnalyzer.class.getClassLoader().getResourceAsStream(resource)), + path.toFile()); + return path; + } + + private static long nanoToMillis(long nano) { + return nano / 1000 / 1000; + } +} diff --git a/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/helper/SimpleFlameGraph.java b/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/helper/SimpleFlameGraph.java new file mode 100644 index 00000000..398ba680 --- /dev/null +++ b/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/helper/SimpleFlameGraph.java @@ -0,0 +1,265 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.helper; + +import org.apache.commons.lang3.tuple.Triple; +import org.eclipse.jifa.jfr.model.JavaThreadCPUTime; +import org.eclipse.jifa.jfr.model.StackTrace; +import org.eclipse.jifa.jfr.model.Frame; +import org.eclipse.jifa.jfr.model.TaskResultBase; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; + +public class SimpleFlameGraph { + public List roots = new ArrayList<>(); + public BigDecimal totalSampleValue = new BigDecimal(0); + + public static SimpleFlameGraph parse(JavaThreadCPUTime tct) { + SimpleFlameGraph g = new SimpleFlameGraph(); + long totalCpuTime = tct.totalCPUTime(); + Map samples = tct.getSamples(); + + if (samples != null) { + AtomicLong stackTraceCount = new AtomicLong(); + samples.values().forEach(stackTraceCount::addAndGet); + long stackTraceCpuTime = totalCpuTime / stackTraceCount.get(); + + samples.keySet().forEach(item -> { + long count = samples.get(item); + g.addStackTrace(item, stackTraceCpuTime * count); + }); + } + + return g; + } + + public static SimpleFlameGraph parse(TaskResultBase ta) { + SimpleFlameGraph g = new SimpleFlameGraph(); + Map samples = ta.getSamples(); + samples.keySet().forEach(item -> { + long count = samples.get(item); + g.addStackTrace(item, count); + }); + + return g; + } + + public void addStackTrace(StackTrace st, long sampleValue) { + totalSampleValue = totalSampleValue.add(new BigDecimal(sampleValue)); + Frame[] frames = st.getFrames(); + if (frames == null || frames.length == 0) { + return; + } + + Frame[] reverseFrames = new Frame[frames.length]; + for (int i = 0; i < frames.length; i++) { + reverseFrames[i] = frames[frames.length - 1 - i]; + } + + addFrames(null, reverseFrames, 0, sampleValue); + } + + public List> queryLeafNodes(double threshold) { + return queryLeafNodes(null, threshold); + } + + public List> queryLeafNodes(String leafKw) { + return queryLeafNodes(leafKw, 0); + } + + public List> queryLeafNodes(String leafKw, double threshold) { + List list = new ArrayList<>(); + this.queryLeafNodes(null, leafKw, threshold, list); + List> result = new ArrayList<>(); + list.forEach(node -> { + double percent = node.getValue().multiply(BigDecimal.valueOf(100)) + .divide(totalSampleValue, 2, RoundingMode.HALF_UP).doubleValue(); + Triple value = + Triple.of(node.getName(), String.valueOf(node.getValue().longValue()), String.valueOf(percent)); + result.add(value); + }); + return result; + } + + public void queryLeafNodes(SimpleFlameGraphNode currentNode, String leafKw, double threshold, + List resultList) { + List children; + if (currentNode == null) { + children = roots; + } else { + children = currentNode.getChildren(); + } + + if (children == null || children.isEmpty()) { + if (currentNode != null) { + double percent = currentNode.getValue().multiply(BigDecimal.valueOf(100)) + .divide(totalSampleValue, 2, RoundingMode.HALF_UP).doubleValue(); + if (percent >= threshold) { + if (leafKw == null || currentNode.getName().contains(leafKw)) { + resultList.add(currentNode); + } + } + } + } else { + for (SimpleFlameGraphNode child : children) { + queryLeafNodes(child, leafKw, threshold, resultList); + } + } + } + + public void clearPrintFlag(SimpleFlameGraphNode currentNode) { + List children; + if (currentNode == null) { + children = roots; + } else { + children = currentNode.getChildren(); + } + + if (currentNode != null) { + currentNode.setPrint(false); + } + + if (children != null && !children.isEmpty()) { + for (SimpleFlameGraphNode child : children) { + clearPrintFlag(child); + } + } + } + + public void matchAndSetPrintFlag(SimpleFlameGraphNode currentNode, String leafKw, double threshold) { + List children; + if (currentNode == null) { + children = roots; + } else { + children = currentNode.getChildren(); + } + + if (children == null || children.isEmpty()) { + if (currentNode != null) { + if (currentNode.getValue().multiply(BigDecimal.valueOf(100)) + .divide(totalSampleValue, RoundingMode.HALF_UP).intValue() >= threshold) { + if (leafKw == null || currentNode.getName().contains(leafKw)) { + currentNode.setPrint(true); + } + } + } + } else { + for (SimpleFlameGraphNode child : children) { + matchAndSetPrintFlag(child, leafKw, threshold); + if (child.isPrint()) { + if (currentNode != null) { + currentNode.setPrint(true); + } + } + } + } + } + + public String dump(double threshold) { + return dump(null, threshold); + } + + public String dump() { + return dump(null); + } + + public String dump(String leafKw) { + return dump(leafKw, 0); + } + + public String dump(String leafKw, double threshold) { + StringBuffer buffer = new StringBuffer(); + dump(buffer, null, 0, leafKw, threshold); + return buffer.toString(); + } + + public void dump(StringBuffer buffer, SimpleFlameGraphNode currentNode, int indent, String leafKw, + double threshold) { + clearPrintFlag(currentNode); + matchAndSetPrintFlag(currentNode, leafKw, threshold); + + List children; + if (currentNode == null) { + children = roots; + } else { + children = currentNode.getChildren(); + } + + StringBuilder padding = new StringBuilder(); + for (int i = 0; i < Math.max(0, indent); i++) { + padding.append(" "); + } + + if (currentNode != null) { + String msg = String.format("%s%s [%d] [%.2f]\n", + padding, currentNode.getName(), currentNode.getValue().longValue(), + currentNode.getValue().multiply(BigDecimal.valueOf(100)) + .divide(totalSampleValue, 2, RoundingMode.HALF_UP).doubleValue()); + buffer.append(msg); + } + + if (children != null && !children.isEmpty()) { + for (SimpleFlameGraphNode child : children) { + if (!child.isPrint()) { + continue; + } + dump(buffer, child, indent + 1, leafKw, threshold); + } + } + } + + private void addFrames(SimpleFlameGraphNode parent, Frame[] frames, int index, long sampleValue) { + if (frames.length == 0 || index > frames.length) { + return; + } + + List children; + if (parent == null) { + children = roots; + } else { + if (parent.getChildren() == null) { + parent.setChildren(new ArrayList<>()); + } + children = parent.getChildren(); + } + + Frame frame = frames[index]; + String desc = frame.toString(); + + Optional result = children.stream().filter(item -> item.getName().equals(desc)).findAny(); + SimpleFlameGraphNode childrenHead; + if (result.isPresent()) { + childrenHead = result.get(); + childrenHead.setValue(childrenHead.getValue().add(BigDecimal.valueOf(sampleValue))); + } else { + childrenHead = new SimpleFlameGraphNode(); + childrenHead.setName(desc); + childrenHead.setValue(BigDecimal.valueOf(sampleValue)); + childrenHead.setParent(parent); + children.add(childrenHead); + } + + if (index + 1 == frames.length) { + return; + } + + addFrames(childrenHead, frames, index + 1, sampleValue); + } +} + diff --git a/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/helper/SimpleFlameGraphNode.java b/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/helper/SimpleFlameGraphNode.java new file mode 100644 index 00000000..28dde39c --- /dev/null +++ b/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/helper/SimpleFlameGraphNode.java @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.helper; + +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.util.List; + +@Setter +@Getter +public class SimpleFlameGraphNode { + private String name; + private BigDecimal value; + + private SimpleFlameGraphNode parent; + private List children; + + private boolean print = false; +} diff --git a/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/util/TestDescriptorUtil.java b/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/util/TestDescriptorUtil.java new file mode 100644 index 00000000..01adbee1 --- /dev/null +++ b/analysis/jfr/src/test/java/org/eclipse/jifa/jfr/util/TestDescriptorUtil.java @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.jfr.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TestDescriptorUtil { + @Test + public void decodeMethodArgsTest() { + DescriptorUtil util = new DescriptorUtil(); + + Assertions.assertEquals(util.decodeMethodArgs( + "(Ljava/lang/String;I[[ZJ)D"), + "(String, int, boolean[][], long)" + // "(java.lang.String, int, boolean[][], long)" + ); + + Assertions.assertEquals(util.decodeMethodArgs( + "(I)V"), + "(int)" + ); + + Assertions.assertEquals(util.decodeMethodArgs( + "(Ljava/io/DataOutput;I)V"), + "(DataOutput, int)" + // "(java.io.DataOutput, int)" + ); + + Assertions.assertEquals(util.decodeMethodArgs( + "(Ljava/lang/Class;Ljava/util/List;Ljava/util/List;)Ljdk/jfr/EventType"), + "(Class, List, List)" + // "(java.lang.Class, java.util.List, java.util.List)" + ); + } +} \ No newline at end of file diff --git a/analysis/jfr/src/test/resources/ap-cpu-20.jfr b/analysis/jfr/src/test/resources/ap-cpu-20.jfr new file mode 100644 index 00000000..57b93ef9 Binary files /dev/null and b/analysis/jfr/src/test/resources/ap-cpu-20.jfr differ diff --git a/analysis/jfr/src/test/resources/ap-cpu-default.jfr b/analysis/jfr/src/test/resources/ap-cpu-default.jfr new file mode 100644 index 00000000..c176ff6a Binary files /dev/null and b/analysis/jfr/src/test/resources/ap-cpu-default.jfr differ diff --git a/analysis/jfr/src/test/resources/ap-wall-20-1.jfr b/analysis/jfr/src/test/resources/ap-wall-20-1.jfr new file mode 100644 index 00000000..fdce91b5 Binary files /dev/null and b/analysis/jfr/src/test/resources/ap-wall-20-1.jfr differ diff --git a/analysis/jfr/src/test/resources/ap-wall-20.jfr b/analysis/jfr/src/test/resources/ap-wall-20.jfr new file mode 100644 index 00000000..08a5315e Binary files /dev/null and b/analysis/jfr/src/test/resources/ap-wall-20.jfr differ diff --git a/analysis/jfr/src/test/resources/ap-wall-default.jfr b/analysis/jfr/src/test/resources/ap-wall-default.jfr new file mode 100644 index 00000000..c1ee00b8 Binary files /dev/null and b/analysis/jfr/src/test/resources/ap-wall-default.jfr differ diff --git a/analysis/jfr/src/test/resources/jfr.jfr b/analysis/jfr/src/test/resources/jfr.jfr new file mode 100644 index 00000000..927e7166 Binary files /dev/null and b/analysis/jfr/src/test/resources/jfr.jfr differ diff --git a/analysis/src/main/java/org/eclipse/jifa/analysis/listener/ProgressListener.java b/analysis/src/main/java/org/eclipse/jifa/analysis/listener/ProgressListener.java index 3446cc38..f45b6cd9 100644 --- a/analysis/src/main/java/org/eclipse/jifa/analysis/listener/ProgressListener.java +++ b/analysis/src/main/java/org/eclipse/jifa/analysis/listener/ProgressListener.java @@ -15,7 +15,7 @@ /** * Progress listener of the analysis. - * Currently, it is only used for the fist analysis. + * Currently, it is only used for the first analysis. */ public interface ProgressListener { diff --git a/common/src/main/java/org/eclipse/jifa/common/annotation/UseGsonEnumAdaptor.java b/common/src/main/java/org/eclipse/jifa/common/annotation/UseGsonEnumAdaptor.java new file mode 100644 index 00000000..5620b5bd --- /dev/null +++ b/common/src/main/java/org/eclipse/jifa/common/annotation/UseGsonEnumAdaptor.java @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +package org.eclipse.jifa.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface UseGsonEnumAdaptor { +} \ No newline at end of file diff --git a/common/src/main/java/org/eclipse/jifa/common/util/EnumTypeAdaptor.java b/common/src/main/java/org/eclipse/jifa/common/util/EnumTypeAdaptor.java new file mode 100644 index 00000000..6fec92d9 --- /dev/null +++ b/common/src/main/java/org/eclipse/jifa/common/util/EnumTypeAdaptor.java @@ -0,0 +1,34 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.jifa.common.util; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +public class EnumTypeAdaptor extends TypeAdapter { + public EnumTypeAdaptor() { + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + out.jsonValue("\"" + value.toString() + "\""); + } + + @Override + public T read(JsonReader in) { + throw new UnsupportedOperationException("Only supports writes."); + } +} \ No newline at end of file diff --git a/common/src/main/java/org/eclipse/jifa/common/util/GsonHolder.java b/common/src/main/java/org/eclipse/jifa/common/util/GsonHolder.java index 16c4eeb0..50b2c9b8 100644 --- a/common/src/main/java/org/eclipse/jifa/common/util/GsonHolder.java +++ b/common/src/main/java/org/eclipse/jifa/common/util/GsonHolder.java @@ -25,6 +25,7 @@ import com.google.gson.TypeAdapterFactory; import com.google.gson.reflect.TypeToken; import org.eclipse.jifa.common.annotation.UseAccessor; +import org.eclipse.jifa.common.annotation.UseGsonEnumAdaptor; import java.lang.reflect.Type; import java.time.LocalDateTime; @@ -39,6 +40,7 @@ public interface GsonHolder { * The gson instance used by all jifa java code */ Gson GSON = new GsonBuilder().registerTypeAdapterFactory(new TypeFactory()) + .registerTypeAdapterFactory(new EnumTypeFactory()) .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter()) .serializeSpecialFloatingPointValues() .create(); @@ -61,6 +63,18 @@ public TypeAdapter create(final Gson gson, final TypeToken type) { } } + class EnumTypeFactory implements TypeAdapterFactory { + + public TypeAdapter create(final Gson gson, final TypeToken type) { + Class t = type.getRawType(); + if (t.isAnnotationPresent(UseGsonEnumAdaptor.class)) { + return new EnumTypeAdaptor<>(); + } else { + return null; + } + } + } + class LocalDateTimeTypeAdapter implements JsonSerializer, JsonDeserializer { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); diff --git a/frontend/components.d.ts b/frontend/components.d.ts index d8884f94..412dee7a 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -9,6 +9,7 @@ declare module 'vue' { export interface GlobalComponents { ElAlert: typeof import('element-plus/es')['ElAlert'] ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete'] + ElAutoResizer: typeof import('element-plus/es')['ElAutoResizer'] ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] @@ -42,6 +43,7 @@ declare module 'vue' { ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTableV2: typeof import('element-plus/es')['ElTableV2'] ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabs: typeof import('element-plus/es')['ElTabs'] ElTag: typeof import('element-plus/es')['ElTag'] diff --git a/frontend/src/components/Analysis.vue b/frontend/src/components/Analysis.vue index 305c7c43..04320c26 100644 --- a/frontend/src/components/Analysis.vue +++ b/frontend/src/components/Analysis.vue @@ -1,5 +1,5 @@ + + + + + + diff --git a/frontend/src/components/jfr/Toolbar.vue b/frontend/src/components/jfr/Toolbar.vue new file mode 100644 index 00000000..5ba3d746 --- /dev/null +++ b/frontend/src/components/jfr/Toolbar.vue @@ -0,0 +1,19 @@ + + + diff --git a/frontend/src/components/jfr/flame-graph.js b/frontend/src/components/jfr/flame-graph.js new file mode 100644 index 00000000..526389ae --- /dev/null +++ b/frontend/src/components/jfr/flame-graph.js @@ -0,0 +1,1961 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +/** + * @author Denghui Dong + */ +{ + const template = document.createElement('template'); + + template.innerHTML = ` + + +
+ + +
+
+ +
+ +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+ +
+ +
+ +
+ +
+
+ Flame Graph Help + x +
+ +
+ +
+
+
+ ^c, ⌘c, ff +
+
Copy the content of the touched frame
+
+ +
+
+ fs +
+
Copy the stack trace from the touched frame
+
+ +
+
+ Downward +
+
+
+
+
+
+
+
+
+
+ `; + + function Frame(flameGraph, raw, depth, isRoot = false) { + this.fg = flameGraph; + this.isRoot = isRoot; + + this.raw = raw; + + this.weight = 0; + this.selfWeight = 0; + this.parent = null; + this.index = -1; + this.hasLeftSide = false; + this.hasRightSide = false; + this.depth = depth; + + this.weightOfBaseline1 = 0; + this.selfWeightOfBaseline1 = 0; + this.weightOfBaseline2 = 0; + this.selfWeightOfBaseline2 = 0; + + this.text = ''; + + this.addWeight = function (weight) { + this.weight += weight; + }; + + this.addWeightOfBaselines = function (weightOfBaseline1, weightOfBaseline2) { + this.weightOfBaseline1 += weightOfBaseline1; + this.weightOfBaseline2 += weightOfBaseline2; + }; + + this.addSelfWeightOfBaselines = function (selfWeightOfBaseline1, selfWeightOfBaseline2) { + this.selfWeightOfBaseline1 += selfWeightOfBaseline1; + this.selfWeightOfBaseline2 += selfWeightOfBaseline2; + }; + + this.addSelfWeight = function (weight) { + this.selfWeight += weight; + }; + + this.setPinned = function () { + this.pinned = true; + if (this.parent !== this.fg.$root) { + this.parent.setPinned(); + } + }; + + this.setSide = function (left) { + if (left) { + this.parent.hasLeftSide = true; + } else { + this.parent.hasRightSide = true; + } + }; + + this.clearSide = function (left) { + if (left) { + this.parent.hasLeftSide = false; + } else { + this.parent.hasRightSide = false; + } + }; + + this.clearFindSide = function () { + if (this.fg.$pinnedFrameLeft) { + this.fg.$pinnedFrameLeft.clearSide(true); + this.fg.$pinnedFrameLeft = null; + } + + if (this.fg.$pinnedFrameRight) { + this.fg.$pinnedFrameRight.clearSide(false); + this.fg.$pinnedFrameRight = null; + } + }; + + this.findSide = function () { + let n = this; + let p = this.parent; + while (n.index === 0) { + if (p === this.fg.$root) { + break; + } + n = p; + p = p.parent; + } + + if (n.index > 0) { + let t = p.children[n.index - 1]; + this.fg.$pinnedFrameLeft = t; + t.setSide(true); + } + + n = this; + p = this.parent; + while (n.index === p.children.length - 1) { + if (p === this.fg.$root) { + break; + } + if (p.selfWeight > 0) { + return; + } + n = p; + p = p.parent; + } + + if (n.index < p.children.length - 1) { + let t = p.children[n.index + 1]; + this.fg.$pinnedFrameRight = t; + t.setSide(false); + } + }; + + this.setUnpinned = function () { + this.pinned = false; + if (this.parent !== this.fg.$root) { + this.parent.setUnpinned(); + } + }; + + this.findOrAddChild = function (raw) { + if (!this.children) { + this.children = []; + } + + for (let i = 0; i < this.children.length; i++) { + const child = this.children[i]; + if (this.fg.$$frameEquator(this.fg.$dataSource, child.raw, raw)) { + return child; + } + } + + return this.addChild(raw); + }; + + this.addChild = function (raw) { + if (!this.children) { + this.children = []; + } + + const child = new Frame(this.fg, raw, this.depth + 1); + child.index = this.children.length; + child.parent = this; + this.children.push(child); + return child; + }; + + this.sort = function () { + if (!this.children) { + return; + } + + if (this.children.length > 1) { + this.children.sort((left, right) => right.weight - left.weight); + } + + for (let i = 0; i < this.children.length; i++) { + this.children[i].index = i; + this.children[i].sort(); + } + }; + + this.diffPercent = function () { + let cp = 0; + if (this.fg.$totalWeightOfBaseline2 > 0) { + cp = this.weightOfBaseline2 / this.fg.$totalWeightOfBaseline2; + } + + let bp = 0; + if (this.fg.$totalWeightOfBaseline1 > 0) { + bp = this.weightOfBaseline1 / this.fg.$totalWeightOfBaseline1; + } + + if (bp > 0) { + let r = (cp - bp) / bp; + if (r > 1) { + return 1; + } + if (r < -1) { + return -1; + } + return r; + } else if (cp > 0) { + return 1; + } else { + return 0; + } + }; + + this.drawSelf = function () { + if (!this.isRoot) { + // generate information and text + this.infomation = this.fg.$$diff + ? { + selfWeight: this.selfWeight, + weight: this.weight, + totalWeight: this.fg.$totalWeight, + + selfWeightOfBaseline1: this.selfWeightOfBaseline1, + weightOfBaseline1: this.weightOfBaseline1, + totalWeightOfBaseline1: this.fg.$totalWeightOfBaseline1, + + selfWeightOfBaseline2: this.selfWeightOfBaseline2, + weightOfBaseline2: this.weightOfBaseline2, + totalWeightOfBaseline2: this.fg.$totalWeightOfBaseline2, + + diffPercent: this.diffPercent() + } + : { + selfWeight: this.selfWeight, + weight: this.weight, + totalWeight: this.fg.$totalWeight + }; + + this.text = this.fg.$$textGenerator(flameGraph.$dataSource, raw, this.infomation); + + this.infomation.text = this.text; + } + if (!this.color) { + this.color = this.fg.$$colorSelector(this.fg.dataSource, this.raw, this.infomation); + } + + this.fg.$context.fillStyle = this.color[0]; + this.fg.$context.fillRect(this.x, this.y, this.width, this.height); + + this.visibleText = null; + if (this.width > this.fg.$showTextWidthThreshold && this.text.length > 0) { + this.fg.$context.font = this.isRoot ? this.fg.$rootFont : this.fg.$font; + this.fg.$context.fillStyle = this.color[1]; + this.fg.$context.textBaseline = 'middle'; + let w = this.fg.$context.measureText(this.text).width; + let leftW = this.width - 2 * this.fg.$textGap; + if (w <= leftW) { + this.fg.$context.fillText( + this.text, + this.x + this.fg.$textGap, + this.y + this.height / 2 + 1 + ); + this.visibleText = this.text; + } else { + // truncate text and append suffix + let len = Math.floor( + (this.text.length * (leftW - this.fg.$context.measureText(this.fg.$moreText).width)) / w + ); + let text = null; + for (let i = len; i > 0; i--) { + text = this.text.substring(0, len) + this.fg.$moreText; + if (this.fg.$context.measureText(text).width <= leftW) { + break; + } + text = null; + } + if (text != null) { + this.fg.$context.fillText( + text, + this.x + this.fg.$textGap, + this.y + this.height / 2 + 1 + ); + } + this.visibleText = text; + } + } + this.fg.$stackTraceMaxDrawnDepth = Math.max(this.depth, this.fg.$stackTraceMaxDrawnDepth); + this.fg.$sibling[this.depth].push(this); + }; + + this.resetPosition = function () { + this.x = 0; + this.y = 0; + this.width = 0; + this.height = 0; + + if (this.children) { + this.children.forEach((c) => c.resetPosition()); + } + }; + + this.draw = function (x, y, w, h) { + this.x = x; + this.y = y; + this.fg.$maxY = Math.max(y + h, this.fg.$maxY); + this.width = w; + this.height = h; + + this.drawSelf(); + + if (this.children) { + if (this.fg.$pinned && this === this.fg.$pinnedFrame) { + this.fg.$drawingChildrenOfPinnedFrame = true; + } + let xGap = this.fg.$xGap; + let childY = this.fg.downward ? y + h + this.fg.$yGap : y - h - this.fg.$yGap; + if ( + !this.fg.$pinned || + this === this.fg.$pinnedFrame || + this.fg.$drawingChildrenOfPinnedFrame + ) { + const space = this.children.length - 1; + let leftWidth = w; + if ((space * xGap) / w > this.fg.$xGapThreashold) { + xGap = 0; + } else { + leftWidth = leftWidth - space * xGap; + } + let endX = x + w; + let nextX = x; + for (let i = 0; i < this.children.length; i++) { + let cw = 0; + if (i === this.children.length - 1 && this.selfWeight === 0) { + cw = endX - nextX; + } else { + cw = (leftWidth * this.children[i].weight) / this.weight; + } + this.children[i].draw(nextX, childY, cw, h); + nextX += cw + xGap; + } + } else { + let sideWidth = 15; + if (this === this.fg.$pinnedFrameLeft || this === this.fg.$pinnedFrameRight) { + this.fg.$drawingChildrenOfSideFrame = true; + this.fg.$drawingLeftSide = this === this.fg.$pinnedFrameLeft; + } + if (this.fg.$drawingChildrenOfSideFrame) { + if (!this.fg.$drawingLeftSide || this.selfWeight === 0) { + for (let i = 0; i < this.children.length; i++) { + if ( + (this.fg.$drawingLeftSide && i === this.children.length - 1) || + (!this.fg.$drawingLeftSide && i === 0) + ) { + this.children[i].draw(x, childY, sideWidth, h); + } else { + this.children[i].resetPosition(); + } + } + } else { + for (let i = 0; i < this.children.length; i++) { + this.children[i].resetPosition(); + } + } + } else { + for (let i = 0; i < this.children.length; i++) { + let xGap = this.fg.$xGap; + if ((xGap * 2) / w > this.fg.$xGapThreashold) { + xGap = 0; + } + if (this.children[i].pinned) { + let cx = x; + let cw = w; + if (this.hasLeftSide) { + cx += sideWidth + xGap; + cw -= sideWidth + xGap; + } + if (this.hasRightSide) { + cw -= sideWidth + xGap; + } else if (this.selfWeight > 0 && this.fg.$pinnedFrame.parent === this) { + cw -= sideWidth; + } + this.children[i].draw(cx, childY, cw, h); + } else if (this.children[i] === this.fg.$pinnedFrameLeft) { + this.children[i].draw(x, childY, sideWidth, h); + } else if (this.children[i] === this.fg.$pinnedFrameRight) { + this.children[i].draw(x + w - sideWidth, childY, sideWidth, h); + } else { + this.children[i].resetPosition(); + } + } + } + if (this === this.fg.$pinnedFrameLeft || this === this.fg.$pinnedFrameRight) { + this.fg.$drawingChildrenOfSideFrame = false; + } + } + if (this.fg.$pinned && this === this.fg.$pinnedFrame) { + this.fg.$drawingChildrenOfPinnedFrame = false; + } + } + }; + + this.contain = function (x, y) { + return x > this.x && x < this.x + this.width && y > this.y && y < this.y + this.height; + }; + + this.maxDepth = function () { + let maxDepth = this.depth; + if (this.children) { + for (let i = 0; i < this.children.length; i++) { + maxDepth = Math.max(maxDepth, this.children[i].maxDepth()); + } + } + return maxDepth; + }; + + function hexToRGB(hex, alpha = 1) { + let r = parseInt(hex.slice(1, 3), 16), + g = parseInt(hex.slice(3, 5), 16), + b = parseInt(hex.slice(5, 7), 16); + + if (hex.length === 9) { + alpha = parseInt(hex.slice(7, 9), 16) / 255; + } + + return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')'; + } + + // noinspection JSUnusedLocalSymbols + this.touch = function (x, y) { + this.fg.$frameMask.style.left = this.x + 'px'; + this.fg.$frameMask.style.top = this.y + 'px'; + + this.fg.$frameMask.style.width = this.width + 'px'; + this.fg.$frameMask.style.height = this.height + 'px'; + this.fg.$frameMask.style.backgroundColor = this.color[0]; + + this.fg.$frameMaskText.style.color = this.color[1]; + this.fg.$frameMaskText.style.paddingLeft = this.fg.$textGap + 'px'; + this.fg.$frameMaskText.style.lineHeight = this.fg.$frameMask.style.height; + this.fg.$frameMaskText.style.fontSize = this === this.fg.$root ? '14px' : '12px'; + this.fg.$frameMaskText.innerText = this.visibleText; + + this.fg.$frameMask.style.cursor = 'pointer'; + this.fg.$frameMask.style.visibility = 'visible'; + this.fg.$frameMask.focus(); + + let top = this.y + this.height - this.fg.$flameGraphInner.scrollTop; + let detailsNode = this.fg.shadowRoot.getElementById('frame-postcard-content-main-details'); + if (detailsNode) { + detailsNode.parentNode.removeChild(detailsNode); + } + + if (this !== this.fg.$root) { + this.fg.$framePostcardContentMain.style.backgroundColor = this.color[0]; + this.fg.$framePostcardContentMain.style.color = this.color[1]; + let hp = Math.round((this.depth / this.maxDepth()) * 100); + let direction = this.fg.downward ? 'to bottom' : 'to top'; + + // title + this.fg.$framePostcardContentMainTitle.innerText = this.fg.$$titleGenerator( + this.fg.$dataSource, + this.raw, + this.infomation + ); + this.fg.$framePostcardContentMainLine.style.background = + 'linear-gradient(' + + direction + + ', ' + + hexToRGB(this.color[1], 0.7) + + ' 0% ' + + hp + + '%, ' + + hexToRGB(this.color[1], 0.2) + + ' ' + + hp + + '% 100%)'; + + // details + let details = this.fg.$$detailsGenerator(this.fg.$dataSource, this.raw, this.infomation); + if (details) { + let keys = Object.keys(details); + let content = null; + if (keys.length > 0) { + content = + '
'; + for (let i = 0; i < keys.length; i++) { + content += '
' + keys[i] + '
'; + content += '
  • ' + details[keys[i]] + '
'; + } + content += '
'; + } + if (content != null) { + let t = document.createElement('template'); + t.innerHTML = content.trim(); + this.fg.$framePostcardContentMain.appendChild(t.content.firstChild); + } + } + + // foot + this.fg.$framePostcardContentFoot.innerText = this.fg.$$footTextGenerator( + this.fg.$dataSource, + this.raw, + this.infomation + ); + let wp = Math.round((this.weight / this.fg.$totalWeight) * 100); + + let footColor = this.fg.$$footColorSelector(this.fg.$dataSource, this.raw, this.infomation); + let startColor, endColor, fontColor; + if (footColor.length > 2) { + startColor = footColor[0]; + endColor = footColor[1]; + fontColor = footColor[2]; + } else { + startColor = endColor = footColor[0]; + fontColor = footColor[2]; + } + this.fg.$framePostcardContentFoot.style.background = + 'linear-gradient(to right, ' + + hexToRGB(startColor) + + ' 0% ' + + wp + + '%, ' + + hexToRGB(endColor) + + ' ' + + wp + + '% 100%)'; + this.fg.$framePostcardContentFoot.style.color = fontColor; + + this.fg.$framePostcardShadow.style.left = x + 'px'; + this.fg.$framePostcardShadow.style.top = top + 'px'; + this.fg.$framePostcard.style.visibility = 'visible'; + this.fg.decideFramePostcardLayout(); + + if (this.fg.$$diff) { + let diffPercent = this.diffPercent(); + let top; + if (diffPercent > 0) { + top = 0.5 * (1 - diffPercent) * 100 + '%'; + } else { + top = (0.5 + 0.5 * -diffPercent) * 100 + '%'; + } + this.fg.$colorArrow.style.top = top; + this.fg.$colorArrow.style.visibility = 'visible'; + } + } + this.fg.$currentFrame = this; + }; + + this.leave = function () { + this.fg.$framePostcard.style.visibility = 'hidden'; + this.fg.$frameMask.style.visibility = 'hidden'; + this.fg.$currentFrame = null; + + if (this.fg.$$diff) { + this.fg.$colorArrow.style.visibility = 'hidden'; + } + }; + + // only clear the children and weight since this method only used by root + this.clear = function () { + this.children = null; + this.weight = 0; + }; + } + + class FlameGraph extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + let sr = this.shadowRoot; + sr.appendChild(template.content.cloneNode(true)); + + this.$canvas = sr.getElementById('flame-graph-canvas'); + this.$context = this.$canvas.getContext('2d'); + this.$context.save(); + + this.$frameHeight = 24; + + this.$fgVGap = 0; + this.$fgVEndGap = 5; + this.$xGap = 0.2; + this.$xGapThreashold = 0.01; + this.$yGap = 0.5; + this.$textGap = 6; + this.$showTextWidthThreshold = 30; + this.$fontFamily = 'Menlo,NotoSans,"Lucida Grande","Lucida Sans Unicode",sans-serif'; + this.$font = '400 12px ' + this.$fontFamily; + this.$font_600 = '600 12px ' + this.$fontFamily; + this.$rootFont = '600 14px ' + this.$fontFamily; + this.$moreText = '...'; + + this.$defaultColorScheme = { + colorForZero: ['#c5c8d3', '#000000'], + colors: [ + ['#761d96', '#ffffff'], + ['#c12561', '#ffffff'], + ['#fec91b', '#000000'], + ['#3f7350', '#ffffff'], + ['#408118', '#ffffff'], + ['#3ea9da', '#000000'], + ['#9fb036', '#ffffff'], + ['#b671c1', '#ffffff'], + ['#faa938', '#000000'] + ] + }; + + this.$maxY = 0; + + this.$flameGraph = sr.getElementById('flame-graph'); + this.$flameGraphInner = sr.getElementById('flame-graph-inner'); + this.$flameGraphInnerWrapper = sr.getElementById('flame-graph-inner-wrapper'); + this.$pinnedFrameMask = sr.getElementById('pinned-frame-mask'); + this.$frameMask = sr.getElementById('frame-mask'); + this.$flameGraphHelp = sr.getElementById('flame-graph-help'); + this.$closeFlameGraphHelp = sr.getElementById('close-flame-graph-help'); + + this.$helpButton = sr.getElementById('help-button'); + + this.$helpButton.addEventListener('click', () => { + if (this.$flameGraphHelp.style.visibility !== 'visible') { + this.$flameGraphHelp.style.visibility = 'visible'; + } else { + this.$flameGraphHelp.style.visibility = 'hidden'; + } + }); + + this.$flameGraphInner.addEventListener('click', () => { + if (this.$flameGraphHelp.style.visibility === 'visible') { + this.$flameGraphHelp.style.visibility = 'hidden'; + } + }); + + this.$closeFlameGraphHelp.addEventListener('click', () => { + this.$flameGraphHelp.style.visibility = 'hidden'; + }); + this.$commandMode = false; + this.$frameMask.addEventListener('keydown', (e) => { + if ((e.key === 'c' || e.key === 'C') && (e.metaKey || e.ctrlKey)) { + this.copy(false); + this.$commandMode = false; + } else { + if (this.$commandMode) { + if (e.key === 'f' || e.key === 'F') { + this.copy(false); + } else if (e.key === 's' || e.key === 'S') { + this.copy(true); + } + this.$commandMode = false; + } else if (e.key === 'f' || e.key === 'F') { + this.$commandMode = true; + } + } + }); + this.$frameMaskText = sr.getElementById('frame-mask-text'); + this.$framePostcard = sr.getElementById('frame-postcard'); + this.$framePostcardShadow = sr.getElementById('frame-postcard-shadow'); + this.$framePostcardConnectingLine = sr.getElementById('frame-postcard-connecting-line'); + this.$framePostcardContent = sr.getElementById('frame-postcard-content'); + this.$framePostcardContentMain = sr.getElementById('frame-postcard-content-main'); + this.$framePostcardContentMainLine = sr.getElementById('frame-postcard-content-main-line'); + this.$framePostcardContentMainTitle = sr.getElementById('frame-postcard-content-main-title'); + this.$framePostcardContentFoot = sr.getElementById('frame-postcard-content-foot'); + + this.$frameMask.style.font = this.$font_600; + + this.$root = null; + + this.$currentFrame = null; + this.$pinned = false; + this.$pinnedFrame = null; + this.$pinnedFrameLeft = null; + this.$pinnedFrameRight = null; + + this.$drawingChildrenOfPinnedFrame = false; + + this.$frameMask.addEventListener('mousemove', (e) => { + this.handleFrameMaskMouseMoveEvent(e); + }); + this.$frameMask.addEventListener('click', (e) => { + this.handleFrameMaskClickEvent(e); + }); + this.$frameMask.addEventListener('dblclick', (e) => { + if (window.getSelection) { + window.getSelection().removeAllRanges(); + } + this.handleFrameMaskClickEvent(e); + }); + + this.$scrollEventListener = () => { + this.handleScrollEvent(); + }; + + this.$flameGraphInner.addEventListener('scroll', this.$scrollEventListener); + + window.addEventListener('scroll', this.$scrollEventListener); + + this.$downwardBunnton = sr.getElementById('downward-button'); + this.$downwardBunnton.addEventListener('click', () => (this.downward = !this.downward)); + + this.$root = new Frame(this, null, 0, true); + + this.$touchedFrame = null; + + this.$canvas.addEventListener('mousemove', (e) => { + this.handleCanvasMouseMoveEvent(e); + }); + + this.$flameGraph.addEventListener('mouseleave', () => { + if (this.$touchedFrame) { + let tf = this.$touchedFrame; + this.$touchedFrame = null; + tf.leave(); + } + }); + + this.$totalWeight = 0; + + let o = this; + new ResizeObserver(function () { + o.render(true, false); + }).observe(this.$flameGraph); + + this.$colorBarDiv = sr.getElementById('color-bar-div'); + this.$colorArrow = sr.getElementById('color-arrow'); + } + + handleScrollEvent() { + this.$currentFrame = null; + this.$touchedFrame = null; + this.$frameMask.style.cursor = 'default'; + this.$frameMask.style.visibility = 'hidden'; + this.$framePostcard.style.visibility = 'hidden'; + + if (this.$stackTraceMaxDrawnDepth < this.$stackTraceMaxDepth) { + if (this.downward) { + if (this.$flameGraphInner.scrollTop > this.$currentScrollTopLimit) { + this.$flameGraphInner.scrollTop = this.$currentScrollTopLimit; + } + } else { + if (this.$flameGraphInner.scrollTop < this.$currentScrollTopLimit) { + this.$flameGraphInner.scrollTop = this.$currentScrollTopLimit; + } + } + } + } + + handleFrameMaskMouseMoveEvent(e) { + this.$framePostcardShadow.style.left = this.$frameMask.offsetLeft + e.offsetX + 'px'; + this.decideFramePostcardLayout(); + e.stopPropagation(); + } + + handleFrameMaskClickEvent(e) { + if (window.getSelection().type === 'Range') { + e.stopPropagation(); + return; + } + + if (this.$currentFrame === this.$root) { + return; + } + + if (!this.$pinned) { + this.$pinned = true; + this.$pinnedFrame = this.$currentFrame; + this.$pinnedFrame.setPinned(); + this.$pinnedFrame.findSide(); + } else { + if (this.$pinnedFrame === this.$currentFrame) { + this.$pinnedFrame.setUnpinned(); + this.$pinnedFrame.clearFindSide(); + this.$pinnedFrame = null; + this.$pinnedFrameMask.style.visibility = 'hidden'; + this.$pinned = false; + } else { + this.$pinnedFrame.setUnpinned(); + this.$pinnedFrame.clearFindSide(); + this.$pinnedFrame = this.$currentFrame; + this.$pinnedFrame.setPinned(); + this.$pinnedFrame.findSide(); + } + } + + this.render(false, false); + + if (this.$pinned && this.$pinnedFrame) { + this.$pinnedFrameMask.style.left = this.$pinnedFrame.x + 'px'; + this.$pinnedFrameMask.style.top = this.$pinnedFrame.y + 'px'; + this.$pinnedFrameMask.style.width = this.$pinnedFrame.width + 'px'; + this.$pinnedFrameMask.style.height = this.$pinnedFrame.height + 'px'; + this.$pinnedFrameMask.style.visibility = 'visible'; + } + + let ne = new Event('mousemove'); + ne.offsetX = this.$frameMask.offsetLeft + e.offsetX; + ne.offsetY = this.$frameMask.offsetTop + e.offsetY; + + this.$framePostcard.style.visibility = 'hidden'; + this.$frameMask.style.cursor = 'default'; + this.$frameMask.style.visibility = 'hidden'; + + this.$canvas.dispatchEvent(ne); + e.stopPropagation(); + } + + render(reInitRenderContext, reGenFrames) { + { + // cache: from dataSource + if (this.$dataSource) { + this.$$isLineFormat = this.$dataSource.format.toLowerCase() === 'line'; + this.$$diff = !!this.$dataSource.diff; + + // cache: from configuration + this.$$dataExtractor = this.dataExtractor; + + this.$$stackTracesCounter = this.stackTracesCounter; + this.$$stackTraceExtractor = this.stackTraceExtractor; + this.$$framesCounter = this.framesCounter; + this.$$frameExtractor = this.frameExtractor; + this.$$framesIndexer = this.framesIndexer; + this.$$stackTraceFilter = this.stackTraceFilter; + this.$$frameEquator = this.frameEquator; + this.$$reverse = this.reverse; + + this.$$rootFramesCounter = this.rootFramesCounter; + this.$$rootFrameExtractor = this.rootFrameExtractor; + this.$$childFramesCounter = this.childFramesCounter; + this.$$childFrameExtractor = this.childFrameExtractor; + this.$$frameStepper = this.frameStepper; + this.$$childFramesIndexer = this.childFramesIndexer; + + this.$$weightsExtractor = this.weightsExtractor; + + this.$$rootTextGenerator = this.rootTextGenerator; + this.$$textGenerator = this.textGenerator; + this.$$titleGenerator = this.titleGenerator; + this.$$detailsGenerator = this.detailsGenerator; + this.$$footTextGenerator = this.footTextGenerator; + + this.$$rootColorSelector = this.rootColorSelector; + this.$$colorSelector = this.colorSelector; + this.$$footColorSelector = this.footColorSelector; + this.$$hashCodeGenerator = this.hashCodeGenerator; + + this.$$showHelpButton = this.showHelpButton; + } + } + + if (reInitRenderContext) { + this.clearState(); + } + + this.clearCanvas(); + + if (reGenFrames) { + this.genFrames(); + } + + if (reInitRenderContext) { + this.initRenderContext(); + } + + if (this.$totalWeight === 0) { + this.$helpButton.style.visibility = 'hidden'; + this.$sibling = null; + return; + } + + if (this.$$diff) { + this.$colorBarDiv.style.display = 'flex'; + } + + if (this.$$showHelpButton) { + this.$helpButton.style.visibility = 'visible'; + this.$fgHGap = 15; + } else { + this.$helpButton.style.visibility = 'hidden'; + this.$fgHGap = 0; + } + + let rect = this.$canvas.getBoundingClientRect(); + + this.$stackTraceMaxDrawnDepth = 0; + this.$sibling = Array(this.$stackTraceMaxDepth + 1); + for (let i = 0; i < this.$stackTraceMaxDepth + 1; i++) { + this.$sibling[i] = []; + } + if (this.downward) { + this.$root.draw( + this.$fgHGap, + this.$fgVGap, + rect.width - this.$fgHGap * 2, + this.$frameHeight + ); + } else { + this.$root.draw( + this.$fgHGap, + rect.height - this.$fgVGap - this.$frameHeight, + rect.width - this.$fgHGap * 2, + this.$frameHeight + ); + } + + if (this.$stackTraceMaxDrawnDepth < this.$stackTraceMaxDepth) { + let height = (this.$stackTraceMaxDrawnDepth + 1) * this.$frameHeight; + + if (this.$stackTraceMaxDrawnDepth > 0) { + height += this.$stackTraceMaxDrawnDepth * this.$yGap; + } + height += this.$fgVGap + this.$fgVEndGap; + + if (this.downward) { + this.$currentScrollTopLimit = Math.max( + height - this.$flameGraphInner.getBoundingClientRect().height, + this.$flameGraphInner.scrollTop + ); + } else { + this.$currentScrollTopLimit = Math.min( + this.$flameGraphHeight - height, + this.$flameGraphInner.scrollTop + ); + } + } + } + + clearState() { + this.$pinnedFrame = null; + this.$currentFrame = null; + this.$touchedFrame = null; + this.$pinnedFrameMask.style.visibility = 'hidden'; + this.$frameMask.style.cursor = 'default'; + this.$frameMask.style.visibility = 'hidden'; + this.$framePostcard.style.visibility = 'hidden'; + this.$pinned = false; + } + + clearCanvas() { + this.$context.clearRect(0, 0, this.$canvas.width, this.$canvas.height); + } + + genFramesFromLineData() { + let dataSource = this.$dataSource; + + for (let i = 0; i < this.$$stackTracesCounter(dataSource); i++) { + const stackTrace = this.$$stackTraceExtractor(dataSource, i); + if (!this.$$stackTraceFilter(dataSource, stackTrace)) { + continue; + } + + const frameCount = this.$$framesCounter(dataSource, stackTrace); + + if (frameCount === 0) { + return; + } + + this.$stackTraceMaxDepth = Math.max(this.$stackTraceMaxDepth, frameCount); + + let weights = this.$$weightsExtractor(dataSource, stackTrace); + let weight, weightOfBaseline1, weightOfBaseline2; + if (this.$$diff) { + [weightOfBaseline1, weightOfBaseline2] = weights; + weight = weightOfBaseline1 + weightOfBaseline2; + this.$totalWeightOfBaseline1 += weightOfBaseline1; + this.$totalWeightOfBaseline2 += weightOfBaseline2; + } else { + weight = weights; + } + + this.$totalWeight += weight; + this.$root.addWeight(weight); + + let current = this.$root; + let j = this.$$reverse ? frameCount - 1 : 0; + let end = this.$$reverse ? -1 : frameCount; + let step = this.$$reverse ? -1 : 1; + for (; j !== end; j += step) { + const frame = this.$$frameExtractor(dataSource, stackTrace, j); + const child = current.findOrAddChild(frame); + child.addWeight(weight); + if (this.$$diff) { + child.addWeightOfBaselines(weightOfBaseline1, weightOfBaseline2); + } + current = child; + } + current.addSelfWeight(weight); + if (this.$$diff) { + current.addSelfWeightOfBaselines(weightOfBaseline1, weightOfBaseline2); + } + } + } + + genFramesFromTreeData() { + const queue = []; + let dataSource = this.$dataSource; + + const process = (parent, frame) => { + let child = parent.addChild(frame); + + let weights = this.$$weightsExtractor(dataSource, frame); + let selfWeight, + weight, + selfWeightOfBaseline1, + weightOfBaseline1, + selfWeightOfBaseline2, + weightOfBaseline2; + if (this.$$diff) { + [selfWeightOfBaseline1, weightOfBaseline1, selfWeightOfBaseline2, weightOfBaseline2] = + weights; + + child.addSelfWeightOfBaselines(selfWeightOfBaseline1, selfWeightOfBaseline2); + child.addWeightOfBaselines(weightOfBaseline1, weightOfBaseline2); + + selfWeight = selfWeightOfBaseline1 + selfWeightOfBaseline2; + weight = weightOfBaseline1 + weightOfBaseline2; + } else { + [selfWeight, weight] = weights; + } + + child.addSelfWeight(selfWeight); + child.addWeight(weight); + + this.$stackTraceMaxDepth = Math.max(this.$stackTraceMaxDepth, child.depth); + + if (this.$$childFramesCounter(dataSource, frame) > 0) { + queue.push(child); + } + return child; + }; + + const rootFramesCount = this.$$rootFramesCounter(dataSource); + for (let i = 0; i < rootFramesCount; i++) { + const rootFrame = process(this.$root, this.$$rootFrameExtractor(dataSource, i)); + this.$totalWeight += rootFrame.weight; + if (this.$$diff) { + this.$totalWeightOfBaseline1 += rootFrame.weightOfBaseline1; + this.$totalWeightOfBaseline2 += rootFrame.weightOfBaseline2; + } + } + + this.$root.addWeight(this.$totalWeight); + + while (queue.length > 0) { + const frame = queue.shift(); + const childrenCount = this.$$childFramesCounter(dataSource, frame.raw); + for (let i = 0; i < childrenCount; i++) { + process(frame, this.$$childFrameExtractor(dataSource, frame.raw, i)); + } + } + } + + genFrames() { + this.$root.clear(); + this.$stackTraceMaxDepth = 0; + this.$totalWeight = 0; + this.$totalWeightOfBaseline1 = 0; + this.$totalWeightOfBaseline2 = 0; + + if (this.$dataSource) { + let format = this.$dataSource.format; + if (format === 'line') { + this.genFramesFromLineData(); + } else if (format === 'tree') { + if (this.$$reverse) { + console.warn("Tree format data doesn't support reverse"); + } + this.genFramesFromTreeData(); + } else { + throw new Error(`Unsupported dataSource format ${format}`); + } + } + + this.$root.sort(); + + this.$information = this.$$diff + ? { + totalWeight: this.$totalWeight, + totalWeightOfBaseline1: this.$totalWeightOfBaseline1, + totalWeightOfBaseline2: this.$totalWeightOfBaseline2 + } + : { + totalWeight: this.$totalWeight + }; + + this.$root.text = this.$$rootTextGenerator(this.$dataSource, this.$information); + } + + initRenderContext() { + let w = this.width; + if (w) { + if (w.endsWith('%')) { + this.$flameGraph.style.width = w; + } else { + this.$flameGraph.style.width = w + 'px'; + } + } + + let h = this.height; + if (h) { + if (h.endsWith('%')) { + this.$flameGraph.style.height = h; + } else { + this.$flameGraph.style.height = h + 'px'; + } + } + + this.$context.restore(); + this.$context.save(); + + let height = (this.$stackTraceMaxDepth + 1) * this.$frameHeight; + + if (this.$stackTraceMaxDepth > 0) { + height += this.$stackTraceMaxDepth * this.$yGap; + } + height += this.$fgVGap + this.$fgVEndGap; + + this.$flameGraphInnerWrapper.style.height = height + 'px'; + this.$flameGraphHeight = height; + + let innerHeight = this.$flameGraphInner.getBoundingClientRect().height; + this.$flameGraphInner.style.overflowY = null; + if (innerHeight < height) { + this.$flameGraphInner.style.overflowY = 'auto'; + } else if (!this.downward) { + this.$flameGraphInnerWrapper.style.height = innerHeight + 'px'; + } + + if (!this.downward) { + this.$flameGraphInner.scrollTop = this.$flameGraphInner.scrollHeight; + this.$downwardBunnton.style.background = 'grey'; + this.$downwardBunnton.style.flexDirection = 'row'; + } else { + this.$flameGraphInner.scrollTop = 0; + this.$downwardBunnton.style.background = 'rgb(24, 144, 255)'; + this.$downwardBunnton.style.flexDirection = 'row-reverse'; + } + + const dpr = window.devicePixelRatio || 1; + const rect = this.$canvas.getBoundingClientRect(); + this.$canvas.width = rect.width * dpr; + this.$canvas.height = rect.height * dpr; + this.$context.scale(dpr, dpr); + + if (this.$dataSource) { + this.$root.color = this.$$rootColorSelector(this.$dataSource, this.$information); + } + this.$colorBarDiv.style.display = 'none'; + } + + connectedCallback() { + this.addEventListener('re-render', () => { + this.render(true, true); + }); + } + + disconnectedCallback() { + window.removeEventListener('scroll', this.$scrollEventListener); + } + + get width() { + return this.getAttribute('width'); + } + + set width(w) { + this.setAttribute('width', w); + } + + get height() { + return this.getAttribute('height'); + } + + set height(h) { + this.setAttribute('height', h); + } + + get downward() { + return this.hasAttribute('downward'); + } + + set downward(downward) { + this.toggleAttribute('downward', !!downward); + } + + static get observedAttributes() { + return ['width', 'height', 'downward']; + } + + attributeChangedCallback(name, oldVal, newVal) { + if (!this.$dataSource || oldVal === newVal) { + return; + } + this.render(true, false); + } + + set dataSource(dataSource) { + if (!dataSource.format) { + throw new Error("Should specify the format of dataSource: 'line' or 'tree'"); + } + if (typeof dataSource.format !== 'string') { + throw new Error('Illegal dataSource format type, must be string'); + } + let format = dataSource.format.toLowerCase(); + if ('line' !== format && 'tree' !== format) { + throw new Error("Illegal dataSource format, must be 'line' or 'tree'"); + } + this.$dataSource = dataSource; + this.render(true, true); + } + + get dataSource() { + return this.$dataSource; + } + + set configuration(configuration) { + if (typeof configuration !== 'object') { + throw new Error('Configuration should be an object'); + } + this.$configuration = configuration; + } + + get configuration() { + return this.$configuration; + } + + getConfigItemOrDefault(name, def) { + if (this.$configuration && this.$configuration[name]) { + return this.$configuration[name]; + } + return def; + } + + _(name, def) { + return this.getConfigItemOrDefault(name, def); + } + + get dataExtractor() { + return this._('dataExtractor', (dataSource) => dataSource.data); + } + + get stackTracesCounter() { + return this._('stackTracesCounter', (dataSource) => this.$$dataExtractor(dataSource).length); + } + + get stackTraceExtractor() { + return this._( + 'stackTraceExtractor', + (dataSource, index) => this.$$dataExtractor(dataSource)[index] + ); + } + + get framesCounter() { + return this._('framesCounter', (dataSource, stackTrace) => { + return stackTrace[this.$$framesIndexer(dataSource, stackTrace)].length; + }); + } + + get frameExtractor() { + return this._('frameExtractor', (dataSource, stackTrace, index) => { + return stackTrace[this.$$framesIndexer(dataSource, stackTrace)][index]; + }); + } + + get framesIndexer() { + return this._('framesIndexer', (dataSource, stackTrace) => 0); + } + + get stackTraceFilter() { + return this._('stackTraceFilter', (dataSource, stackTrace) => true); + } + + get frameEquator() { + return this._('frameEquator', (dataSource, left, right) => { + return left === right; + }); + } + + get reverse() { + return !!this._('reverse', false); + } + + get rootFramesCounter() { + return this._( + 'rootFramesCounter', + (dataSource) => + this.$$dataExtractor(dataSource).length / this.$$frameStepper(dataSource, null) + ); + } + + get rootFrameExtractor() { + return this._('rootFrameExtractor', (dataSource, index) => { + let steps = this.$$frameStepper(dataSource, null); + const start = index * steps; + return this.$$dataExtractor(dataSource).slice(start, start + steps); + }); + } + + get childFramesCounter() { + return this._('childFramesCounter', (dataSource, frame) => { + return ( + frame[this.$$childFramesIndexer(dataSource, frame)].length / + this.$$frameStepper(dataSource, frame) + ); + }); + } + + get childFrameExtractor() { + return this._('childFrameExtractor', (dataSource, frame, index) => { + let steps = this.$$frameStepper(dataSource, frame); + const start = index * steps; + return frame[this.$$childFramesIndexer(dataSource, frame)].slice(start, start + steps); + }); + } + + get frameStepper() { + return this._( + 'frameStepper', + this.$$diff ? (dataSource, frame) => 6 : (dataSource, frame) => 4 + ); + } + + get childFramesIndexer() { + return this._( + 'childFramesIndexer', + this.$$diff ? (dataSource, frame) => 5 : (dataSource, frame) => 3 + ); + } + + get weightsExtractor() { + return this._( + 'weightsExtractor', + this.$$isLineFormat + ? this.$$diff + ? // 1 -> weightOfBaseline1 + // 2 -> weightOfBaseline2 + (dataSource, input) => [input[1], input[2]] + : // 1 -> weight + (dataSource, input) => input[1] + : this.$$diff + ? // 1 -> selfWeightOfBaseline1 + // 2 -> weightOfBaseline1 + // 3 -> selfWeightOfBaseline2 + // 4 -> weightOfBaseline2 + (dataSource, input) => [input[1], input[2], input[3], input[4]] + : // 1 -> selfWeight + // 2 -> weight + (dataSource, input) => [input[1], input[2]] + ); + } + + get rootTextGenerator() { + return this._('rootTextGenerator', (dataSource, information) => { + let totalWeight = information.totalWeight.toLocaleString(); + if (this.$$diff) { + let totalWeightOfBaseline1 = information.totalWeightOfBaseline1.toLocaleString(); + let totalWeightOfBaseline2 = information.totalWeightOfBaseline2.toLocaleString(); + return `Total: ${totalWeight} (Baseline1: ${totalWeightOfBaseline1}, Baseline2: ${totalWeightOfBaseline2})`; + } + return `Total: ${totalWeight}`; + }); + } + + get textGenerator() { + return this._( + 'textGenerator', + this.$$isLineFormat + ? (dataSource, frame, information) => frame + : (dataSource, frame, information) => frame[0] + ); + } + + get titleGenerator() { + return this._('titleGenerator', (dataSource, frame, information) => information.text); + } + + get detailsGenerator() { + return this._('detailsGenerator', (dataSource, frame, information) => null); + } + + get footTextGenerator() { + return this._('footTextGenerator', (dataSource, frame, information) => { + let selfWeight = information.selfWeight; + let weight = information.weight; + let totalWeight = information.totalWeight; + let value = Math.round((weight / totalWeight) * 100 * 100) / 100; + return `${value.toLocaleString()}% - (${selfWeight.toLocaleString()}, ${weight.toLocaleString()}, ${totalWeight.toLocaleString()})`; + }); + } + + get rootColorSelector() { + return this._('rootColorSelector', (dataSource, information) => ['#537e8b', '#ffffff']); + } + + get colorSelector() { + return this._('colorSelector', (dataSource, frame, information) => { + if (this.$$diff) { + return [this.diffColor(information.diffPercent), '#ffffff']; + } + let hashCode = this.$$hashCodeGenerator(dataSource, frame, information); + if (hashCode === 0) { + return this.$defaultColorScheme.colorForZero; + } + let colorIndex = Math.abs(hashCode) % this.$defaultColorScheme.colors.length; + if (!colorIndex && colorIndex !== 0) { + colorIndex = 0; + } + return this.$defaultColorScheme.colors[colorIndex]; + }); + } + + get footColorSelector() { + return this._('footColorSelector', (dataSource, frame, information) => { + return ['#537e8bff', '#373b46e6', '#ffffff']; + }); + } + + get hashCodeGenerator() { + return this._('hashCodeGenerator', (dataSource, frame, information) => { + let text = information.text; + let hash = 0; + for (let i = 0; i < text.length; i++) { + hash = 31 * hash + (text.charCodeAt(i) & 0xff); + hash &= 0xffffffff; + } + return hash; + }); + } + + get showHelpButton() { + return !!this._('showHelpButton', false); + } + + hexColorToFloatColor(hex) { + return [ + parseInt(hex.substring(1, 3), 16) / 255, + parseInt(hex.substring(3, 5), 16) / 255, + parseInt(hex.substring(5, 7), 16) / 255 + ]; + } + + floatToHex(f) { + let v = Math.round(f); + let r = v.toString(16); + if (r.length === 1) { + return '0' + r; + } + return r; + } + + floatColorToHexColor(float) { + return ( + '#' + + this.floatToHex(float[0] * 255) + + this.floatToHex(float[1] * 255) + + this.floatToHex(float[2] * 255) + ); + } + + linearColor(from, to, pct) { + return [ + from[0] + (to[0] - from[0]) * pct, + from[1] + (to[1] - from[1]) * pct, + from[2] + (to[2] - from[2]) * pct + ]; + } + + diffColor(diffPercent) { + let from = '#808080'; + let to = '#FF0000'; + if (diffPercent < 0) { + to = '#008000'; + if (diffPercent < -1) { + diffPercent = -1; + } + } else if (diffPercent > 1) { + diffPercent = 1; + } + + from = this.hexColorToFloatColor(from); + to = this.hexColorToFloatColor(to); + return this.floatColorToHexColor(this.linearColor(from, to, Math.abs(diffPercent))); + } + + findFrame(x, y) { + if (!this.$sibling) { + return null; + } + + let index; + if (this.downward) { + if (y <= this.$root.y) { + return null; + } + index = Math.floor((y - this.$root.y) / (this.$frameHeight + this.$yGap)); + } else { + if (y >= this.$root.y + this.$frameHeight) { + return null; + } + index = Math.floor( + (this.$root.y + this.$frameHeight - y) / (this.$frameHeight + this.$yGap) + ); + } + + if (index >= this.$sibling.length || this.$sibling[index].length === 0) { + return null; + } + + let frame = this.$sibling[index][0]; + if (y <= frame.y || y >= frame.y + frame.height) { + return null; + } + + let start = 0; + let end = this.$sibling[index].length - 1; + while (start <= end) { + const mid = (start + end) >>> 1; + frame = this.$sibling[index][mid]; + if (x <= frame.x) { + end = mid - 1; + } else if (x >= frame.x + frame.width) { + start = mid + 1; + } else { + return frame; + } + } + return null; + } + + handleCanvasMouseMoveEvent(e) { + let lastTouchedFrame = this.$touchedFrame; + + if (lastTouchedFrame) { + if (lastTouchedFrame.contain(e.offsetX, e.offsetY)) { + lastTouchedFrame.touch(e.offsetX, e.offsetY); + return; + } + } + + this.$touchedFrame = this.findFrame(e.offsetX, e.offsetY); + + if (lastTouchedFrame !== null && lastTouchedFrame !== this.$touchedFrame) { + lastTouchedFrame.leave(); + } + + if (this.$touchedFrame) { + this.$touchedFrame.touch(e.offsetX, e.offsetY); + } + e.stopPropagation(); + } + + decideFramePostcardLayout() { + let rect = this.$framePostcardShadow.getBoundingClientRect(); + + this.$framePostcard.style.left = rect.left + 'px'; + this.$framePostcard.style.top = rect.top + 'px'; + + let height = this.$framePostcardContent.getBoundingClientRect().height + 26; + + let showAtTop = rect.top - height < 0; + if (showAtTop) { + this.$framePostcardContent.style.top = '26px'; + this.$framePostcardContent.style.bottom = null; + } else { + this.$framePostcardContent.style.top = null; + this.$framePostcardContent.style.bottom = '26px'; + } + let showAtLeft = + rect.left + 392 > (window.innerWidth || document.documentElement.clientWidth); + if (showAtLeft) { + this.$framePostcardContentMain.style.marginLeft = + 366 - this.$framePostcardContentMain.clientWidth + 'px'; + this.$framePostcardContent.style.left = '-392px'; + if (showAtTop) { + this.$framePostcardConnectingLine.style.transform = + 'rotate(135deg) translate3d(0px, -.5px, 0)'; + } else { + this.$framePostcardConnectingLine.style.transform = + 'rotate(-135deg) translate3d(0px, -.5px, 0)'; + } + } else { + this.$framePostcardContentMain.style.marginLeft = '0px'; + this.$framePostcardContent.style.left = '26px'; + if (showAtTop) { + this.$framePostcardConnectingLine.style.transform = + 'rotate(45deg) translate3d(0px, -.5px, 0)'; + } else { + this.$framePostcardConnectingLine.style.transform = + 'rotate(-45deg) translate3d(0px, -.5px, 0)'; + } + } + } + + copy(stackTrace) { + let text = this.$touchedFrame.text; + if (stackTrace) { + let f = this.$touchedFrame.parent; + while (f && f !== this.$root) { + text += '\n' + f.text; + f = f.parent; + } + } + if (navigator.clipboard && window.isSecureContext && false) { + navigator.clipboard.writeText(text).then(() => { + this.dispatchEvent( + new CustomEvent('copied', { + detail: { + text: text + } + }) + ); + }); + } else { + let textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + // noinspection JSDeprecatedSymbols + let success = document.execCommand('copy'); + textArea.remove(); + // re-focus + this.$frameMask.focus(); + if (success) { + this.dispatchEvent( + new CustomEvent('copied', { + detail: { + text: text + } + }) + ); + } + } + } + } + + window.customElements.define('flame-graph', FlameGraph); +} diff --git a/frontend/src/components/jfr/utils.ts b/frontend/src/components/jfr/utils.ts new file mode 100644 index 00000000..ec165057 --- /dev/null +++ b/frontend/src/components/jfr/utils.ts @@ -0,0 +1,113 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +export const toReadableValue = (unit: string, value: number) => { + if (unit === 'ns') { + let result = ''; + // to ns + const ns = value % 1000000; + + // to ms + value = Math.round(value / 1000000); + const ms = value % 1000; + if (ms > 0) { + result = ms + 'ms'; + } + + // to second + value = Math.floor(value / 1000); + const s = value % 60; + if (s > 0) { + if (result.length > 0) { + result = s + 's ' + result; + } else { + result = s + 's'; + } + } + + // to minute + value = Math.floor(value / 60); + const m = value; + if (m > 0) { + if (result.length > 0) { + result = m.toLocaleString() + 'm ' + result; + } else { + result = m.toLocaleString() + 'm'; + } + } + + if (result.length === 0) { + if (ns > 0) { + return ns + 'ns'; + } else { + return '0ms'; + } + } + + return result; + } else if (unit === 'byte') { + let result = ''; + const bytes = value % 1024; + + // to Kilobytes + value = Math.round(value / 1024); + const kb = value % 1024; + if (kb > 0) { + result = kb + 'KB'; + } + + // to Megabytes + value = Math.floor(value / 1024); + const mb = value % 1024; + if (mb > 0) { + if (result.length > 0) { + result = mb + 'MB ' + result; + } else { + result = mb + 'MB'; + } + } + + // to Gigabyte + value = Math.floor(value / 1024); + const gb = value % 1024; + if (gb > 0) { + if (result.length > 0) { + result = gb + 'GB ' + result; + } else { + result = gb + 'GB'; + } + } + + // to Terabyte + value = Math.floor(value / 1024); + const tb = value; + if (tb > 0) { + if (result.length > 0) { + result = tb + 'TB ' + result; + } else { + result = tb + 'TB'; + } + } + + if (result.length === 0) { + if (bytes == 0) { + return '0B'; + } else { + return bytes + 'bytes'; + } + } + + return result; + } else { + return value.toLocaleString(); + } +}; diff --git a/frontend/src/composables/file-types.ts b/frontend/src/composables/file-types.ts index dd614b22..cefeb48e 100644 --- a/frontend/src/composables/file-types.ts +++ b/frontend/src/composables/file-types.ts @@ -20,6 +20,9 @@ import HeapDumpToolBar from '@/components/heapdump/Toolbar.vue'; import ThreadDump from '@/components/threaddump/ThreadDump.vue'; import ThreadDumpToolBar from '@/components/threaddump/Toolbar.vue'; +import Jfr from '@/components/jfr/Jfr.vue'; +import JfrToolBar from '@/components/jfr/Toolbar.vue'; + export const fileTypeMap = new Map(); export class FileType { @@ -74,3 +77,5 @@ export const THREAD_DUMP = def( null, ThreadDump ); + +export const JFR = def('JFR_FILE', 'jfr', 'jfr-file', JfrToolBar, null, Jfr); diff --git a/frontend/src/i18n/en.ts b/frontend/src/i18n/en.ts index b4537161..3fc8c4bf 100644 --- a/frontend/src/i18n/en.ts +++ b/frontend/src/i18n/en.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2023, 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,6 +13,7 @@ import heapDump from './heapdump/en' import gclog from './gclog/en' import threadDump from './threaddump/en' +import jfr from '@/i18n/jfr/en' export default { jifa: { @@ -58,6 +59,7 @@ export default { heapDump: 'Heap Dump', GCLog: 'GC Log', threadDump: 'Thread Dump', + jfr: 'JFR File', new: 'New File', @@ -122,5 +124,6 @@ export default { heapDump, gclog, threadDump, + jfr, } -}; \ No newline at end of file +}; diff --git a/frontend/src/i18n/jfr/en.ts b/frontend/src/i18n/jfr/en.ts new file mode 100644 index 00000000..8889c50f --- /dev/null +++ b/frontend/src/i18n/jfr/en.ts @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +export default { + option: { + }, + + flameGraph: { + copyMethod: '(Press [CMD] + [C] or [Ctrl] + [C] to copy method)', + }, + + placeholder: { + threadName: 'Please enter the thread name' + } +} diff --git a/frontend/src/i18n/jfr/zh.ts b/frontend/src/i18n/jfr/zh.ts new file mode 100644 index 00000000..94ba88ad --- /dev/null +++ b/frontend/src/i18n/jfr/zh.ts @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +export default { + option: { + }, + + flameGraph: { + copyMethod: '(复制方法名:[CMD] + [C] 或者 [Ctrl] + [C])', + }, + + placeholder: { + threadName: '请输入线程名,支持模糊匹配' + } +} diff --git a/frontend/src/i18n/zh.ts b/frontend/src/i18n/zh.ts index dc35fd52..ae874b4f 100644 --- a/frontend/src/i18n/zh.ts +++ b/frontend/src/i18n/zh.ts @@ -13,6 +13,7 @@ import heapDump from './heapdump/zh' import gclog from './gclog/zh' import threadDump from './threaddump/zh' +import jfr from '@/i18n/jfr/zh' export default { jifa: { @@ -58,6 +59,7 @@ export default { heapDump: '堆内存快照', GCLog: 'GC 日志', threadDump: '线程快照', + jfr: 'JFR 文件', new: '新文件', @@ -122,5 +124,6 @@ export default { heapDump, gclog, threadDump, + jfr } -}; \ No newline at end of file +}; diff --git a/frontend/src/stores/env.ts b/frontend/src/stores/env.ts index 8d3da7fb..516dad1a 100644 --- a/frontend/src/stores/env.ts +++ b/frontend/src/stores/env.ts @@ -34,7 +34,7 @@ export interface HandshakeResponse { publicKey: PublicKey; oauth2LoginLinks: object; user?: User; - disabledFileTransferMethods: [], + disabledFileTransferMethods: []; } const tokenKey = 'jifa-token'; @@ -92,7 +92,7 @@ export const useEnv = defineStore('env', { this.loginFormVisible = true; } - this.disabledFileTransferMethods = data.disabledFileTransferMethods + this.disabledFileTransferMethods = data.disabledFileTransferMethods; }, logout() { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index a8462832..f8ab3b26 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2023, 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -21,7 +21,13 @@ import {ElementPlusResolver} from 'unplugin-vue-components/resolvers' export default defineConfig({ plugins: [ - vue(), + vue({ + template: { + compilerOptions: { + isCustomElement: (tag) => ['flame-graph'].includes(tag), + } + } + }), vueJsx(), AutoImport({ imports: [ @@ -60,4 +66,4 @@ export default defineConfig({ build: { chunkSizeWarningLimit: 10240 } -}) \ No newline at end of file +}) diff --git a/jifa.gradle b/jifa.gradle index 77fce1ba..f4d48037 100644 --- a/jifa.gradle +++ b/jifa.gradle @@ -27,6 +27,7 @@ dependencies { jacocoAggregation project(':analysis:heap-dump:impl') jacocoAggregation project(':analysis:heap-dump:provider') jacocoAggregation project(':analysis:thread-dump') + jacocoAggregation project(':analysis:jfr') jacocoAggregation project(':server') } diff --git a/server/server.gradle b/server/server.gradle index 9e0238b3..13ea7cd0 100644 --- a/server/server.gradle +++ b/server/server.gradle @@ -27,6 +27,7 @@ dependencies { implementation project(':analysis') runtimeOnly project(':analysis:heap-dump:provider') + runtimeOnly project(':analysis:jfr') runtimeOnly project(':analysis:gc-log') runtimeOnly project(':analysis:thread-dump') diff --git a/server/src/main/java/org/eclipse/jifa/server/enums/FileType.java b/server/src/main/java/org/eclipse/jifa/server/enums/FileType.java index f6d0e50f..bd5295fd 100644 --- a/server/src/main/java/org/eclipse/jifa/server/enums/FileType.java +++ b/server/src/main/java/org/eclipse/jifa/server/enums/FileType.java @@ -20,7 +20,9 @@ public enum FileType { GC_LOG("gc-log"), - THREAD_DUMP("thread-dump" ); + THREAD_DUMP("thread-dump"), + + JFR_FILE("jfr-file"); private final String storageDirectoryName; diff --git a/settings.gradle b/settings.gradle index 35abce5f..b8a0b11f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,6 +21,7 @@ include ':analysis:heap-dump:provider' include ':analysis:heap-dump:hook' include ':analysis:gc-log' include ':analysis:thread-dump' +include ':analysis:jfr' include ':server' include ':frontend' diff --git a/site/docs/.vitepress/config.mts b/site/docs/.vitepress/config.mts index 6b64864d..ece7f707 100644 --- a/site/docs/.vitepress/config.mts +++ b/site/docs/.vitepress/config.mts @@ -62,6 +62,7 @@ export default defineConfig({ {text: 'Heap Dump Analysis', link: '/guide/heap-dump-analysis'}, {text: 'GC Log Analysis', link: '/guide/gc-log-analysis'}, {text: 'Thread Dump Analysis', link: '/guide/thread-dump-analysis'}, + {text: 'JFR Analysis', link: '/guide/jfr-analysis'}, ] }, { @@ -135,6 +136,7 @@ export default defineConfig({ {text: '堆快照分析', link: '/zh/guide/heap-dump-analysis'}, {text: 'GC 日志分析', link: '/zh/guide/gc-log-analysis'}, {text: '线程快照分析', link: '/zh/guide/thread-dump-analysis'}, + {text: 'JFR 分析', link: '/zh/guide/jfr-analysis'}, ] }, { diff --git a/site/docs/guide/jfr-analysis.md b/site/docs/guide/jfr-analysis.md new file mode 100644 index 00000000..608a2726 --- /dev/null +++ b/site/docs/guide/jfr-analysis.md @@ -0,0 +1,66 @@ +# JFR Analysis + +## Introduction + +Profiling is a form of dynamic program analysis, which can improve application performance, reduce IT costs, and improve user experience. +Java Flight Recorder (JFR for short) is a profiling tool built into Java. +After JFR is turned on, performance data of various dimensions can be continuously collected from the Java process and JFR files can be generated. +Through the analysis of JFR files, it can help locate performance problems in multiple dimensions such as CPU usage, memory allocation, Socket IO, File IO, and Lock contention. + +## Views + +### CPU + +This view uses flame graph as the main display form, which can help quickly locate the reason why Java consumes high CPU, accurate to the method level, and can be filtered by Thread, Class, or Method. +The underlying JFR Events: +- jdk.ExecutionSample +- jdk.NativeMethodSample + +### Allocation + +If the Java program's memory throughput is high (for example, YoungGC is very frequent), it is likely that some code has created a large number of objects or arrays. +Using this view, you can easily find the method that requests the most memory, that is, the memory allocation hotspot. +The underlying JFR Events: +- jdk.ObjectAllocationInNewTLAB +- jdk.ObjectAllocationOutsideTLAB +- jdk.ObjectAllocationSample (JDK 16 and above, TODO) + +### Wall Clock + +This view is different from CPU hotspots. For CPU hotspots, threads will only be recorded when executing on the CPU, but threads may also be blocked or waiting. In this case, the CPU hotspot view is not very helpful, and the wall clock hotspot comes in handy. Wall clock hotspot does not distinguish between thread running status (e.g. executing on the CPU or blocked). Using the wall clock view, you can see the execution time of each method in the thread and find the method that takes up the most time. Note: The JFR that comes with the JDK does not have separate wall clock data. You can use the wall clock mode of [async-profiler](https://github.com/async-profiler/async-profiler) to generate a JFR file. +The underlying JFR Events: +- jdk.ExecutionSample + +### Lock + +When multiple threads compete for a lock, the threads that cannot grab the lock will be blocked, which may cause performance problems. This view can help locate lock contention hot spots. +The underlying JFR Events: +- jdk.JavaMonitorEnter +- jdk.ThreadPark + +### Socket IO + +This view can help locate Socket IO read/write hotspot methods and find out the method with the most Socket IO times or the longest time. +The underlying JFR Events: +- jdk.SocketRead +- jdk.SocketWrite + +### File IO + +This view can help find the method with the most file IO times or the longest time. +The underlying JFR Events: +- jdk.FileWrite +- jdk.FileRead +- jdk.FileForce + +### Class Load + +This view can help find out the method that triggers class loading the most times or takes the longest time. +The underlying JFR Events: +- jdk.ClassLoad + +### Thead Sleep + +This view can help locate the method that calls Thread.sleep and sleeps the longest time. +The underlying JFR Events: +- jdk.ThreadSleep \ No newline at end of file diff --git a/site/docs/guide/what-is-eclipse-jifa.md b/site/docs/guide/what-is-eclipse-jifa.md index 3107be39..3e0a0e83 100644 --- a/site/docs/guide/what-is-eclipse-jifa.md +++ b/site/docs/guide/what-is-eclipse-jifa.md @@ -11,6 +11,7 @@ Currently, Jifa primarily supports the following features: - [Heap Dump Analysis](./heap-dump-analysis.md) - [GC Log Analysis](./gc-log-analysis.md) - [Thread Dump Analysis](./thread-dump-analysis.md) +- [JFR Analysis](./jfr-analysis.md) In terms of the design, Jifa consists of two main parts: diff --git a/site/docs/index.md b/site/docs/index.md index 984d5dbf..50402f07 100644 --- a/site/docs/index.md +++ b/site/docs/index.md @@ -30,4 +30,8 @@ features: link: ./guide/thread-dump-analysis icon: 🔒 details: "Features: Thread & Thread Pool Analysis, Java Monitors Analysis, Aggregated Stack Trace Views, etc." + - title: JFR Analysis + link: ./guide/jfr-analysis + icon: 🧬 + details: "Features: Parse JFR files and generate hotspot views for CPU, Memory Allocation, Lock, File IO, Socket IO, Wall Clock and other dimensions. Can help locate various application performance issues." --- \ No newline at end of file diff --git a/site/docs/zh/guide/jfr-analysis.md b/site/docs/zh/guide/jfr-analysis.md new file mode 100644 index 00000000..1626a1a2 --- /dev/null +++ b/site/docs/zh/guide/jfr-analysis.md @@ -0,0 +1,67 @@ +# JFR 分析 + +## 介绍 + +性能剖析是一种分析应用程序性能的方法,可以改善应用性能、降低IT成本、提升用户体验。 +Java Flight Recorder(简称JFR)是内建在Java种的性能剖析工具。 +开启JFR后,可以从Java进程中持续收集各种维度的性能数据,并生成JFR文件。 +通过对JFR文件的分析,可以帮助定位CPU占用、内存申请、网络IO、文件IO、锁争抢等多种维度的性能问题。 + +## 视图 + +### CPU热点 + +此视图以火焰图为主要展现形式,可以帮助快速定位Java占用CPU高的的原因,精确到方法级别,并且可以按线程、类、方法进行过滤。 +主要依赖的JFR事件: +- jdk.ExecutionSample +- jdk.NativeMethodSample + +### 内存申请热点 + +如果Java程序内存吞出较高(比如YoungGC较频繁),很可能是某些代码创建了大量的对象或者数组。 +使用此视图可以很方便的找出内存申请最多的方法,即内存申请热点。 +主要依赖的JFR事件: +- jdk.ObjectAllocationInNewTLAB +- jdk.ObjectAllocationOutsideTLAB +- jdk.ObjectAllocationSample (JDK 16及以上, TODO) + +### 墙钟热点 + +此视图和CPU热点有所不同。对于CPU热点,线程只有在CPU上执行时才会被记录到CPU热点中,但线程也有可能被阻塞或者主动等待,对于这种情况,CPU热点视图帮助不大,而墙钟热点视图则正好适合。墙钟热点不区分线程运行状态,无论线程在CPU上执行或者被阻塞,都会进行记录。使用墙钟视图,可以看出线程中每个方法的执行时间的大小,找出占用时间最多的方法。 +注:JDK自带的JFR没有单独的墙钟数据,可以用[async-profiler](https://github.com/async-profiler/async-profiler)的墙钟(wall)模式来生成JFR文件。 +主要依赖的JFR事件: +- jdk.ExecutionSample + +### 锁争抢热点 + +当多个线程去争抢一个锁时,抢不到锁的线程会阻塞等待,可能导致性能问题。此视图可以帮助定位锁争抢热点方法。 +主要依赖的JFR事件: +- jdk.JavaMonitorEnter +- jdk.ThreadPark + +### 网络IO热点 + +此视图可以帮助定位Socket IO读/写热点方法,找出Socket IO次数最多或者时间最长的方法。 +主要依赖的JFR事件: +- jdk.SocketRead +- jdk.SocketWrite + +### 文件IO热点 + +此视图可以帮助定位文件 IO读/写热点方法,找出文件 IO次数最多或者时间最长的方法。 +主要依赖的JFR事件: +- jdk.FileWrite +- jdk.FileRead +- jdk.FileForce + +### 类加载热点 + +此视图可以帮助定位触发类加载的热点方法,找出触发类加载次数最多或者时间最长的方法。 +主要依赖的JFR事件: +- jdk.ClassLoad + +### 线程Sleep + +此视图可以帮助定位调用Thread.sleep的热点方法。 +主要依赖的JFR事件: +- jdk.ThreadSleep \ No newline at end of file diff --git a/site/docs/zh/guide/what-is-eclipse-jifa.md b/site/docs/zh/guide/what-is-eclipse-jifa.md index 7d25e1b2..fc6e3cd0 100644 --- a/site/docs/zh/guide/what-is-eclipse-jifa.md +++ b/site/docs/zh/guide/what-is-eclipse-jifa.md @@ -10,6 +10,7 @@ Eclipse Jifa(简称 Jifa)的名字由 “**J**ava **I**ssues **F**inding **A - [堆快照分析](./heap-dump-analysis.md) - [GC 日志分析](./gc-log-analysis.md) - [线程快照分析](./thread-dump-analysis.md) +- [JFR 分析](./jfr-analysis.md) 在设计上,由两部分组成: diff --git a/site/docs/zh/index.md b/site/docs/zh/index.md index 64d3ec44..5e063407 100644 --- a/site/docs/zh/index.md +++ b/site/docs/zh/index.md @@ -30,5 +30,9 @@ features: link: ./guide/thread-dump-analysis icon: 🔒 details: 功能:线程与线程池分析、Java monitors 分析、调用栈聚合等。可辅助开发者排查 CPU 高、线程泄漏、死锁等问题。 + - title: JFR 分析 + link: ./guide/jfr-analysis + icon: 🧬 + details: 功能:解析JFR文件,生成CPU、内存申请、锁、文件IO、Socket IO、墙钟等维度的热点视图。可以帮助定位各种应用性能问题。 ---