/
Igor_Lakienko
/
profiler
Обзор
Документация
Войти
/
Igor_Lakienko
/
profiler
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
JfrViewer.java
1 831 строка
77 KB
Igor Lakienko
init
04 авг 2026, 14:53
04 авг 2026, 14:53
4b159ae
Код
Авторство
О чём код?
import java.awt.*; import java.awt.event.*; import java.io.*; import java.nio.file.*; import java.time.*; import java.time.format.*; import java.util.*; import java.util.List; import java.util.concurrent.*; import javax.swing.*; import javax.swing.filechooser.*; import jdk.jfr.consumer.*; /** * JFR Viewer — автономный анализатор Java Flight Recorder. * Один файл, ноль внешних зависимостей, только JDK 21. * Запуск: java JfrViewer.java [файл.jfr] [--text] [--top=N] [--thread=NAME] [--help] */ public class JfrViewer { // ========================= Модель данных (раздел 4.1) ========================= static class Sample { String thread; String[] frames; // лист первый (frames[0] = лист), как отдаёт JFR long weight; // 1 для CPU, байты для аллокации, нс для блокировок String allocClass; // null для CPU Sample(String thread, String[] frames, long weight, String allocClass) { this.thread = thread; this.frames = frames; this.weight = weight; this.allocClass = allocClass; } } static class Profile { List<Sample> cpu = new ArrayList<>(); List<Sample> alloc = new ArrayList<>(); List<Sample> block = new ArrayList<>(); Map<String, Long> eventCounts = new TreeMap<>(); TreeSet<String> threads = new TreeSet<>(); Instant first, last; Path source; } static class Node { String name; long self, total; Map<String, Node> children = new LinkedHashMap<>(); List<Node> sortedCache; Node(String name) { this.name = name; } List<Node> sortedChildren() { if (sortedCache == null) { sortedCache = new ArrayList<>(children.values()); sortedCache.sort((a, b) -> Long.compare(b.total, a.total)); } return sortedCache; } } // ========================= Глобальное состояние ========================= private JFrame frame; private JTabbedPane tabs; private Profile profile; private Profile originalProfile; private JComboBox<String> threadCombo; private boolean suppressComboEvents = false; // Аргументы командной строки private static boolean textMode = false; private static int topN = 30; private static String threadFilter = null; private static Path filePath = null; // ========================= Точка входа ========================= public static void main(String[] args) { // Разбор аргументов for (int i = 0; i < args.length; i++) { String arg = args[i]; if ("--help".equals(arg) || "-h".equals(arg)) { printHelp(); return; } else if ("--text".equals(arg)) { textMode = true; } else if (arg.startsWith("--top=")) { try { topN = Integer.parseInt(arg.substring(6)); if (topN <= 0) topN = 30; } catch (NumberFormatException e) { System.err.println("\u041e\u0448\u0438\u0431\u043a\u0430: \u043d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 --top: " + arg); topN = 30; } } else if (arg.startsWith("--thread=")) { threadFilter = arg.substring(9); } else if (!arg.startsWith("-")) { filePath = Path.of(arg); } else { System.err.println("\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442: " + arg); printHelp(); return; } } if (textMode) { // Текстовый режим — без GUI if (filePath == null) { System.err.println("\u041e\u0448\u0438\u0431\u043a\u0430: \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 --text \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 .jfr"); System.exit(1); } runTextMode(filePath); } else { // GUI режим SwingUtilities.invokeLater(() -> { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception ignored) {} JfrViewer viewer = new JfrViewer(); viewer.createAndShowGUI(); if (filePath != null) { viewer.loadFileAsync(filePath); } }); } } private static void printHelp() { System.out.println("JFR Viewer \u2014 \u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 Java Flight Recorder"); System.out.println(); System.out.println("\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435:"); System.out.println(" java JfrViewer.java [\u0444\u0430\u0439\u043b.jfr] [\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b]"); System.out.println(); System.out.println("\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b:"); System.out.println(" --help \u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u044d\u0442\u0443 \u0441\u043f\u0440\u0430\u0432\u043a\u0443"); System.out.println(" --text \u0422\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0439 \u0440\u0435\u0436\u0438\u043c (\u0431\u0435\u0437 GUI, \u0432\u044b\u0432\u043e\u0434 \u0432 stdout)"); System.out.println(" --top=N \u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0441\u0442\u0440\u043e\u043a \u0432 \u0442\u043e\u043f\u0435 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e 30)"); System.out.println(" --thread=NAME \u0424\u0438\u043b\u044c\u0442\u0440 \u043f\u043e \u0438\u043c\u0435\u043d\u0438 \u043f\u043e\u0442\u043e\u043a\u0430"); System.out.println(); System.out.println("\u041f\u0440\u0438\u043c\u0435\u0440\u044b:"); System.out.println(" java JfrViewer.java recording.jfr"); System.out.println(" java JfrViewer.java recording.jfr --text --top=50"); System.out.println(" java JfrViewer.java recording.jfr --thread=main"); System.out.println(); System.out.println("\u0411\u0435\u0437 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u044f \u0444\u0430\u0439\u043b\u0430 \u043e\u0442\u043a\u0440\u043e\u0435\u0442\u0441\u044f \u0434\u0438\u0430\u043b\u043e\u0433 \u0432\u044b\u0431\u043e\u0440\u0430 \u0444\u0430\u0439\u043b\u0430."); System.out.println(); System.out.println("Windows / PowerShell:"); System.out.println(" chcp 65001 (UTF-8 \u043a\u043e\u043d\u0441\u043e\u043b\u044c, \u043e\u0434\u0438\u043d \u0440\u0430\u0437)"); System.out.println(" java -Dfile.encoding=UTF-8 -jar jfrviewer.jar bench.jfr"); System.out.println(" java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -jar jfrviewer.jar bench.jfr --text"); } // ========================= GUI ========================= private void createAndShowGUI() { frame = new JFrame("JFR Viewer"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(1400, 900); frame.setLocationRelativeTo(null); // Меню JMenuBar menuBar = new JMenuBar(); JMenu fileMenu = new JMenu("\u0424\u0430\u0439\u043b"); JMenuItem openItem = new JMenuItem("\u041e\u0442\u043a\u0440\u044b\u0442\u044c..."); openItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); openItem.addActionListener(e -> openFile()); fileMenu.add(openItem); JMenuItem saveReportItem = new JMenuItem("\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043e\u0442\u0447\u0451\u0442..."); saveReportItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); saveReportItem.addActionListener(e -> saveReport()); fileMenu.add(saveReportItem); fileMenu.addSeparator(); JMenuItem exitItem = new JMenuItem("\u0412\u044b\u0445\u043e\u0434"); exitItem.addActionListener(e -> System.exit(0)); fileMenu.add(exitItem); menuBar.add(fileMenu); frame.setJMenuBar(menuBar); // Тулбар с фильтром потоков JPanel toolbarPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 2)); toolbarPanel.setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 4)); toolbarPanel.add(new JLabel("\u041f\u043e\u0442\u043e\u043a:")); threadCombo = new JComboBox<>(new String[]{"\u0412\u0441\u0435 \u043f\u043e\u0442\u043e\u043a\u0438"}); threadCombo.setEnabled(false); threadCombo.addActionListener(e -> { if (!suppressComboEvents && originalProfile != null) { applyThreadFilter(); buildTabs(); } }); toolbarPanel.add(threadCombo); frame.add(toolbarPanel, BorderLayout.NORTH); // Вкладки tabs = new JTabbedPane(); frame.add(tabs, BorderLayout.CENTER); frame.setVisible(true); // Если файл не задан — показать диалог выбора if (filePath == null) { SwingUtilities.invokeLater(this::openFile); } } private void openFile() { JFileChooser chooser = new JFileChooser(); chooser.setDialogTitle("\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0444\u0430\u0439\u043b JFR"); chooser.setFileFilter(new FileNameExtensionFilter("Java Flight Recorder (*.jfr)", "jfr")); if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) { loadFileAsync(chooser.getSelectedFile().toPath()); } } private void saveReport() { if (profile == null) { JOptionPane.showMessageDialog(frame, "\u041d\u0435\u0442 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0444\u0438\u043b\u044f.", "\u041e\u0448\u0438\u0431\u043a\u0430", JOptionPane.WARNING_MESSAGE); return; } JFileChooser chooser = new JFileChooser(); chooser.setDialogTitle("\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043e\u0442\u0447\u0451\u0442"); chooser.setSelectedFile(new File("jfr-report.txt")); if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) { try { String report = generateTextReport(profile, topN, null); Files.writeString(chooser.getSelectedFile().toPath(), report); JOptionPane.showMessageDialog(frame, "\u041e\u0442\u0447\u0451\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d.", "\u0413\u043e\u0442\u043e\u0432\u043e", JOptionPane.INFORMATION_MESSAGE); } catch (IOException ex) { JOptionPane.showMessageDialog(frame, "\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u043f\u0438\u0441\u0438: " + ex.getMessage(), "\u041e\u0448\u0438\u0431\u043a\u0430", JOptionPane.ERROR_MESSAGE); } } } // ========================= Загрузка файла (SwingWorker) ========================= private void loadFileAsync(Path path) { JDialog progressDialog = new JDialog(frame, "\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430...", true); JProgressBar progressBar = new JProgressBar(); progressBar.setIndeterminate(true); progressBar.setStringPainted(true); progressBar.setString("\u0427\u0442\u0435\u043d\u0438\u0435 " + path.getFileName() + "..."); JButton cancelBtn = new JButton("\u041e\u0442\u043c\u0435\u043d\u0430"); JPanel panel = new JPanel(new BorderLayout(10, 10)); panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); panel.add(progressBar, BorderLayout.CENTER); panel.add(cancelBtn, BorderLayout.SOUTH); progressDialog.add(panel); progressDialog.setSize(400, 120); progressDialog.setLocationRelativeTo(frame); progressDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); SwingWorker<Profile, Long> worker = new SwingWorker<>() { @Override protected Profile doInBackground() throws Exception { return load(path, count -> publish(count)); } @Override protected void process(List<Long> chunks) { if (!chunks.isEmpty()) { long count = chunks.get(chunks.size() - 1); progressBar.setString("\u041f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u0439: " + count); } } @Override protected void done() { progressDialog.dispose(); try { originalProfile = get(); frame.setTitle("JFR Viewer \u2014 " + path.getFileName()); suppressComboEvents = true; threadCombo.removeAllItems(); threadCombo.addItem("\u0412\u0441\u0435 \u043f\u043e\u0442\u043e\u043a\u0438"); for (String t : originalProfile.threads) { threadCombo.addItem(t); } threadCombo.setEnabled(true); threadCombo.setSelectedIndex(0); suppressComboEvents = false; applyThreadFilter(); buildTabs(); } catch (CancellationException e) { // Пользователь отменил } catch (Exception e) { Throwable cause = e.getCause() != null ? e.getCause() : e; String msg = cause.getMessage(); if (msg == null || msg.isEmpty()) msg = cause.getClass().getSimpleName(); JOptionPane.showMessageDialog(frame, "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b:\n" + msg, "\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438", JOptionPane.ERROR_MESSAGE); } } }; cancelBtn.addActionListener(e -> { worker.cancel(true); progressDialog.dispose(); }); worker.execute(); progressDialog.setVisible(true); // блокирует EDT до закрытия } private void buildTabs() { tabs.removeAll(); tabs.addTab("\u0421\u0432\u043e\u0434\u043a\u0430", buildTabSummary()); tabs.addTab("\u0413\u043e\u0440\u044f\u0447\u0438\u0435 \u043c\u0435\u0442\u043e\u0434\u044b", buildTabHotMethods()); tabs.addTab("\u0414\u0435\u0440\u0435\u0432\u043e \u0432\u044b\u0437\u043e\u0432\u043e\u0432", buildTabCallTree()); tabs.addTab("\u041e\u0431\u0440\u0430\u0442\u043d\u043e\u0435 \u0434\u0435\u0440\u0435\u0432\u043e", buildTabReverseTree()); tabs.addTab("Flame Graph", buildTabFlameGraph()); tabs.addTab("\u0410\u043b\u043b\u043e\u043a\u0430\u0446\u0438\u0438", buildTabAllocations()); tabs.revalidate(); tabs.repaint(); } private void applyThreadFilter() { if (originalProfile == null) return; int idx = threadCombo.getSelectedIndex(); if (idx <= 0) { profile = originalProfile; } else { String threadName = (String) threadCombo.getSelectedItem(); profile = filterProfileByThread(originalProfile, threadName); } } private static Profile filterProfileByThread(Profile orig, String threadName) { Profile p = new Profile(); p.source = orig.source; p.first = orig.first; p.last = orig.last; p.eventCounts = orig.eventCounts; p.threads = orig.threads; for (Sample s : orig.cpu) { if (threadName.equals(s.thread)) p.cpu.add(s); } for (Sample s : orig.alloc) { if (threadName.equals(s.thread)) p.alloc.add(s); } for (Sample s : orig.block) { if (threadName.equals(s.thread)) p.block.add(s); } return p; } // ========================= Вкладки (T3-T8) ========================= JPanel buildTabSummary() { JPanel root = new JPanel(new BorderLayout(10, 10)); root.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); // ---- Верхняя панель: общая информация ---- JPanel infoPanel = new JPanel(new GridBagLayout()); infoPanel.setBorder(BorderFactory.createTitledBorder("\u041e\u0431\u0449\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f")); GridBagConstraints gc = new GridBagConstraints(); gc.anchor = GridBagConstraints.WEST; gc.insets = new Insets(3, 8, 3, 8); int row = 0; // 1. Имя и размер файла String fileName = profile.source.getFileName().toString(); String fileSize; try { fileSize = JfrViewer.formatBytes(Files.size(profile.source)); } catch (IOException ex) { fileSize = "\u043d/\u0434"; } addInfoRow(infoPanel, gc, row++, "\u0424\u0430\u0439\u043b:", fileName + " (" + fileSize + ")"); // 2. Интервал записи и длительность if (profile.first != null && profile.last != null) { DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") .withZone(ZoneId.systemDefault()); String interval = dtf.format(profile.first) + " \u2014 " + dtf.format(profile.last); addInfoRow(infoPanel, gc, row++, "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0437\u0430\u043f\u0438\u0441\u0438:", interval); Duration dur = Duration.between(profile.first, profile.last); long totalSec = dur.getSeconds(); long hours = totalSec / 3600; long minutes = (totalSec % 3600) / 60; long seconds = totalSec % 60; String durStr; if (hours > 0) { durStr = String.format("%d \u0447 %d \u043c\u0438\u043d %d \u0441\u0435\u043a", hours, minutes, seconds); } else if (minutes > 0) { durStr = String.format("%d \u043c\u0438\u043d %d \u0441\u0435\u043a", minutes, seconds); } else { durStr = String.format("%d \u0441\u0435\u043a", seconds); } addInfoRow(infoPanel, gc, row++, "\u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c:", durStr); } else { addInfoRow(infoPanel, gc, row++, "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0437\u0430\u043f\u0438\u0441\u0438:", "\u043d/\u0434"); addInfoRow(infoPanel, gc, row++, "\u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c:", "\u043d/\u0434"); } // 3. Число потоков addInfoRow(infoPanel, gc, row++, "\u041f\u043e\u0442\u043e\u043a\u043e\u0432:", String.format("%,d", profile.threads.size())); // 4. Количество CPU-сэмплов addInfoRow(infoPanel, gc, row++, "CPU-\u0441\u044d\u043c\u043f\u043b\u043e\u0432:", String.format("%,d", profile.cpu.size())); // 5. Суммарные аллокации long totalAllocBytes = 0; for (Sample s : profile.alloc) { totalAllocBytes += s.weight; } String allocStr = JfrViewer.formatBytes(totalAllocBytes) + " (" + String.format("%,d", profile.alloc.size()) + " \u0441\u043e\u0431\u044b\u0442\u0438\u0439)"; addInfoRow(infoPanel, gc, row++, "\u0410\u043b\u043b\u043e\u043a\u0430\u0446\u0438\u0438:", allocStr); // ---- Предупреждения ---- JPanel warningsPanel = new JPanel(); warningsPanel.setLayout(new BoxLayout(warningsPanel, BoxLayout.Y_AXIS)); boolean hasWarnings = false; // CPU-сэмплов < 100 if (profile.cpu.size() < 100) { JLabel warn = new JLabel("\u26a0 \u0417\u0430\u043f\u0438\u0441\u044c \u043f\u043e\u0447\u0442\u0438 \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u044d\u043c\u043f\u043b\u043e\u0432: \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0441\u0442\u0430\u0438\u0432\u0430\u043b\u043e \u043b\u0438\u0431\u043e \u0437\u0430\u043f\u0438\u0441\u044c \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043a\u043e\u0440\u043e\u0442\u043a\u0430\u044f"); warn.setFont(warn.getFont().deriveFont(Font.BOLD, 16f)); warn.setForeground(Color.RED); warn.setBorder(BorderFactory.createEmptyBorder(5, 8, 5, 8)); warn.setAlignmentX(Component.LEFT_ALIGNMENT); warningsPanel.add(warn); hasWarnings = true; } // Нет jdk.ObjectAllocationSample в eventCounts if (!profile.eventCounts.containsKey("jdk.ObjectAllocationSample")) { JLabel warn = new JLabel("\u26a0 \u0417\u0430\u043f\u0438\u0441\u044c \u0441\u0434\u0435\u043b\u0430\u043d\u0430 \u0431\u0435\u0437 settings=profile, \u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e \u0430\u043b\u043b\u043e\u043a\u0430\u0446\u0438\u044f\u043c \u043d\u0435\u0442"); warn.setFont(warn.getFont().deriveFont(Font.BOLD, 16f)); warn.setForeground(Color.RED); warn.setBorder(BorderFactory.createEmptyBorder(5, 8, 5, 8)); warn.setAlignmentX(Component.LEFT_ALIGNMENT); warningsPanel.add(warn); hasWarnings = true; } // Собираем верхнюю часть: info + warnings JPanel topPanel = new JPanel(); topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS)); infoPanel.setAlignmentX(Component.LEFT_ALIGNMENT); topPanel.add(infoPanel); if (hasWarnings) { warningsPanel.setBorder(BorderFactory.createTitledBorder("\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f")); warningsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); topPanel.add(Box.createVerticalStrut(8)); topPanel.add(warningsPanel); } root.add(topPanel, BorderLayout.NORTH); // ---- 6. Таблица «тип события → количество» ---- String[] columnNames = {"\u0422\u0438\u043f \u0441\u043e\u0431\u044b\u0442\u0438\u044f", "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e"}; Object[][] data = new Object[profile.eventCounts.size()][2]; int idx = 0; for (Map.Entry<String, Long> entry : profile.eventCounts.entrySet()) { data[idx][0] = entry.getKey(); data[idx][1] = entry.getValue(); idx++; } javax.swing.table.AbstractTableModel tableModel = new javax.swing.table.AbstractTableModel() { private final String[] cols = columnNames; private final Object[][] rows = data; @Override public int getRowCount() { return rows.length; } @Override public int getColumnCount() { return cols.length; } @Override public String getColumnName(int column) { return cols[column]; } @Override public Object getValueAt(int rowIndex, int columnIndex) { return rows[rowIndex][columnIndex]; } @Override public Class<?> getColumnClass(int columnIndex) { if (columnIndex == 1) return Long.class; return String.class; } }; JTable table = new JTable(tableModel); table.setAutoCreateRowSorter(true); table.getColumnModel().getColumn(0).setPreferredWidth(400); table.getColumnModel().getColumn(1).setPreferredWidth(120); table.setFillsViewportHeight(true); JScrollPane scrollPane = new JScrollPane(table); scrollPane.setBorder(BorderFactory.createTitledBorder("\u0422\u0438\u043f\u044b \u0441\u043e\u0431\u044b\u0442\u0438\u0439")); root.add(scrollPane, BorderLayout.CENTER); return root; } /** Вспомогательный метод для добавления строки «метка: значение» в GridBagLayout. */ private void addInfoRow(JPanel panel, GridBagConstraints gc, int row, String label, String value) { gc.gridx = 0; gc.gridy = row; gc.weightx = 0; JLabel lbl = new JLabel(label); lbl.setFont(lbl.getFont().deriveFont(Font.BOLD)); panel.add(lbl, gc); gc.gridx = 1; gc.weightx = 1.0; panel.add(new JLabel(value), gc); } JPanel buildTabHotMethods() { JPanel panel = new JPanel(new BorderLayout(0, 4)); // --- Данные --- Map<String, Node> hotMap = computeHotMethods(profile.cpu); long totalSamples = 0; for (Sample s : profile.cpu) totalSamples += s.weight; final long totalSamplesF = totalSamples; List<Node> rows = new ArrayList<>(hotMap.values()); rows.sort((a, b) -> Long.compare(b.self, a.self)); // --- Модель таблицы --- String[] columnNames = {"Self", "Self %", "Total", "Total %", "\u041c\u0435\u0442\u043e\u0434"}; javax.swing.table.AbstractTableModel model = new javax.swing.table.AbstractTableModel() { @Override public int getRowCount() { return rows.size(); } @Override public int getColumnCount() { return 5; } @Override public String getColumnName(int col) { return columnNames[col]; } @Override public Class<?> getColumnClass(int col) { return switch (col) { case 0, 2 -> Long.class; case 1, 3 -> Double.class; case 4 -> String.class; default -> Object.class; }; } @Override public Object getValueAt(int row, int col) { Node n = rows.get(row); return switch (col) { case 0 -> n.self; case 1 -> totalSamplesF > 0 ? 100.0 * n.self / totalSamplesF : 0.0; case 2 -> n.total; case 3 -> totalSamplesF > 0 ? 100.0 * n.total / totalSamplesF : 0.0; case 4 -> n.name; default -> null; }; } }; JTable table = new JTable(model); table.setAutoCreateRowSorter(false); // --- Сортировка --- javax.swing.table.TableRowSorter<javax.swing.table.AbstractTableModel> sorter = new javax.swing.table.TableRowSorter<>(model); table.setRowSorter(sorter); sorter.toggleSortOrder(0); // по Self, ascending sorter.toggleSortOrder(0); // по Self, descending // --- Формат колонок --- table.getColumnModel().getColumn(1).setCellRenderer(new javax.swing.table.DefaultTableCellRenderer() { { setHorizontalAlignment(RIGHT); } @Override protected void setValue(Object value) { setText(value == null ? "" : String.format("%.1f%%", (Double) value)); } }); table.getColumnModel().getColumn(3).setCellRenderer(new javax.swing.table.DefaultTableCellRenderer() { { setHorizontalAlignment(RIGHT); } @Override protected void setValue(Object value) { setText(value == null ? "" : String.format("%.1f%%", (Double) value)); } }); // Self и Total — выравнивание вправо javax.swing.table.DefaultTableCellRenderer rightRenderer = new javax.swing.table.DefaultTableCellRenderer(); rightRenderer.setHorizontalAlignment(javax.swing.table.DefaultTableCellRenderer.RIGHT); table.getColumnModel().getColumn(0).setCellRenderer(rightRenderer); table.getColumnModel().getColumn(2).setCellRenderer(rightRenderer); table.setFillsViewportHeight(true); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // --- Панель фильтров --- JPanel filterPanel = new JPanel(new BorderLayout(6, 0)); filterPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); JTextField filterField = new JTextField(); filterField.setToolTipText("\u0424\u0438\u043b\u044c\u0442\u0440 \u043f\u043e \u0438\u043c\u0435\u043d\u0438 \u043c\u0435\u0442\u043e\u0434\u0430"); JCheckBox hideJdk = new JCheckBox("\u0421\u043a\u0440\u044b\u0442\u044c JDK-\u043c\u0435\u0442\u043e\u0434\u044b"); filterPanel.add(new JLabel("\u0424\u0438\u043b\u044c\u0442\u0440: "), BorderLayout.WEST); filterPanel.add(filterField, BorderLayout.CENTER); filterPanel.add(hideJdk, BorderLayout.EAST); // --- Логика фильтрации --- Runnable applyFilter = () -> { List<RowFilter<Object, Object>> filters = new ArrayList<>(); String text = filterField.getText().trim(); if (!text.isEmpty()) { filters.add(RowFilter.regexFilter(java.util.regex.Pattern.quote(text), 4)); } if (hideJdk.isSelected()) { filters.add(new RowFilter<Object, Object>() { @Override public boolean include(Entry<?, ?> entry) { String method = (String) entry.getValue(4); return method != null && !method.startsWith("java.") && !method.startsWith("javax.") && !method.startsWith("jdk.") && !method.startsWith("sun.") && !method.startsWith("com.sun."); } }); } if (filters.isEmpty()) { sorter.setRowFilter(null); } else { sorter.setRowFilter(RowFilter.andFilter(filters)); } }; filterField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { @Override public void insertUpdate(javax.swing.event.DocumentEvent e) { applyFilter.run(); } @Override public void removeUpdate(javax.swing.event.DocumentEvent e) { applyFilter.run(); } @Override public void changedUpdate(javax.swing.event.DocumentEvent e) { applyFilter.run(); } }); hideJdk.addActionListener(e -> applyFilter.run()); // --- Контекстное меню --- JPopupMenu contextMenu = new JPopupMenu(); JMenuItem showBacktraces = new JMenuItem("\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0431\u0440\u0430\u0442\u043d\u044b\u0435 \u0442\u0440\u0430\u0441\u0441\u044b"); showBacktraces.addActionListener(e -> { if (tabs.getTabCount() > 3) { tabs.setSelectedIndex(3); } }); contextMenu.add(showBacktraces); table.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { int row = table.rowAtPoint(e.getPoint()); if (row >= 0) { table.setRowSelectionInterval(row, row); contextMenu.show(table, e.getX(), e.getY()); } } } }); // --- Сборка --- panel.add(filterPanel, BorderLayout.NORTH); panel.add(new JScrollPane(table), BorderLayout.CENTER); return panel; } JPanel buildTabCallTree() { JPanel panel = new JPanel(new BorderLayout()); if (profile == null || profile.cpu.isEmpty()) { panel.add(new JLabel("\u041d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445 CPU-\u043f\u0440\u043e\u0444\u0438\u043b\u044f \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u044f \u0434\u0435\u0440\u0435\u0432\u0430 \u0432\u044b\u0437\u043e\u0432\u043e\u0432.", SwingConstants.CENTER), BorderLayout.CENTER); return panel; } Node root = buildForwardTree(profile.cpu); // Собственная реализация TreeModel поверх Node CallTreeModel model = new CallTreeModel(root); JTree tree = new JTree(model); tree.setRootVisible(true); tree.setShowsRootHandles(true); tree.setCellRenderer(new CallTreeCellRenderer(root.total)); // Автоматическое раскрытие самой горячей ветки на 15 уровней expandHottestPath(tree, model, root, 15); JScrollPane scrollPane = new JScrollPane(tree); panel.add(scrollPane, BorderLayout.CENTER); return panel; } private void expandHottestPath(JTree tree, CallTreeModel model, Node root, int maxDepth) { javax.swing.tree.TreePath path = new javax.swing.tree.TreePath(root); Node current = root; for (int depth = 0; depth < maxDepth; depth++) { List<Node> children = current.sortedChildren(); if (children.isEmpty()) break; tree.expandPath(path); // Самый горячий ребёнок — первый после сортировки (по total убыванию) Node hottest = children.get(0); path = path.pathByAddingChild(hottest); current = hottest; } // Раскрываем последний уровень и прокручиваем к нему tree.expandPath(path); tree.scrollPathToVisible(path); tree.setSelectionPath(path); } JPanel buildTabReverseTree() { JPanel panel = new JPanel(new BorderLayout()); if (profile == null || profile.cpu.isEmpty()) { panel.add(new JLabel("\u041d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445 CPU-\u043f\u0440\u043e\u0444\u0438\u043b\u044f \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u044f \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0434\u0435\u0440\u0435\u0432\u0430.", SwingConstants.CENTER)); return panel; } Node root = buildReverseTree(profile.cpu); ReverseNodeTreeModel model = new ReverseNodeTreeModel(root); JTree tree = new JTree(model); tree.setRootVisible(true); tree.setShowsRootHandles(true); tree.setCellRenderer(new ReverseNodeTreeRenderer(root.total)); // Авто-раскрытие горячей ветки на 15 уровней expandHotPathReverse(tree, model, root, 15); JScrollPane scroll = new JScrollPane(tree); panel.add(scroll, BorderLayout.CENTER); return panel; } private void expandHotPathReverse(JTree tree, ReverseNodeTreeModel model, Node node, int maxDepth) { javax.swing.tree.TreePath path = new javax.swing.tree.TreePath(node); for (int depth = 0; depth < maxDepth; depth++) { tree.expandPath(path); List<Node> children = node.sortedChildren(); if (children.isEmpty()) break; Node hottest = children.get(0); path = path.pathByAddingChild(hottest); node = hottest; } tree.scrollPathToVisible(path); } JPanel buildTabFlameGraph() { JPanel panel = new JPanel(new BorderLayout(0, 4)); // --- Верхняя панель управления --- JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 2)); JLabel srcLabel = new JLabel("\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a:"); JComboBox<String> sourceCombo = new JComboBox<>(new String[]{"CPU-\u0441\u044d\u043c\u043f\u043b\u044b", "\u0410\u043b\u043b\u043e\u043a\u0430\u0446\u0438\u0438"}); toolbar.add(srcLabel); toolbar.add(sourceCombo); toolbar.add(Box.createHorizontalStrut(12)); JLabel hlLabel = new JLabel("\u041f\u043e\u0434\u0441\u0432\u0435\u0442\u0438\u0442\u044c:"); JTextField highlightField = new JTextField(20); toolbar.add(hlLabel); toolbar.add(highlightField); toolbar.add(Box.createHorizontalStrut(12)); JButton backBtn = new JButton("\u041d\u0430\u0437\u0430\u0434"); JButton resetBtn = new JButton("\u0421\u0431\u0440\u043e\u0441"); toolbar.add(backBtn); toolbar.add(resetBtn); panel.add(toolbar, BorderLayout.NORTH); // --- Flame Graph панель --- FlamePanel flamePanel = new FlamePanel(); JScrollPane scrollPane = new JScrollPane(flamePanel); scrollPane.getVerticalScrollBar().setUnitIncrement(20); panel.add(scrollPane, BorderLayout.CENTER); // --- Инициализация --- Runnable rebuildTree = () -> { int idx = sourceCombo.getSelectedIndex(); List<Sample> samples = (idx == 1) ? profile.alloc : profile.cpu; Node root = buildForwardTree(samples); flamePanel.setRoot(root, idx == 1); }; rebuildTree.run(); // --- Обработчики --- sourceCombo.addActionListener(e -> rebuildTree.run()); highlightField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { private void update() { flamePanel.setHighlight(highlightField.getText()); } @Override public void insertUpdate(javax.swing.event.DocumentEvent e) { update(); } @Override public void removeUpdate(javax.swing.event.DocumentEvent e) { update(); } @Override public void changedUpdate(javax.swing.event.DocumentEvent e) { update(); } }); backBtn.addActionListener(e -> flamePanel.zoomBack()); resetBtn.addActionListener(e -> flamePanel.zoomReset()); // Backspace = Назад panel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) .put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "flameBack"); panel.getActionMap().put("flameBack", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { flamePanel.zoomBack(); } }); return panel; } JPanel buildTabAllocations() { JPanel panel = new JPanel(new BorderLayout()); if (profile.alloc.isEmpty()) { panel.add(new JLabel("\u0414\u0430\u043d\u043d\u044b\u0445 \u043f\u043e \u0430\u043b\u043b\u043e\u043a\u0430\u0446\u0438\u044f\u043c \u043d\u0435\u0442", SwingConstants.CENTER)); return panel; } // === Агрегация по классам === Map<String, long[]> classMap = new LinkedHashMap<>(); for (Sample s : profile.alloc) { String cls = s.allocClass != null ? s.allocClass : "<unknown>"; long[] acc = classMap.computeIfAbsent(cls, k -> new long[2]); acc[0] += s.weight; // суммарный объём acc[1]++; // количество событий } // Сортированный список классов по объёму убывание List<Map.Entry<String, long[]>> classesSorted = new ArrayList<>(classMap.entrySet()); classesSorted.sort((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0])); // === Левая таблица: классы === String[] leftCols = {"\u041a\u043b\u0430\u0441\u0441", "\u041e\u0431\u044a\u0451\u043c", "\u0421\u043e\u0431\u044b\u0442\u0438\u0439"}; Object[][] leftData = new Object[classesSorted.size()][3]; for (int i = 0; i < classesSorted.size(); i++) { Map.Entry<String, long[]> entry = classesSorted.get(i); leftData[i][0] = entry.getKey(); leftData[i][1] = entry.getValue()[0]; leftData[i][2] = entry.getValue()[1]; } AllocTableModel leftModel = new AllocTableModel(leftData, leftCols); JTable leftTable = new JTable(leftModel); leftTable.setAutoCreateRowSorter(false); javax.swing.table.TableRowSorter<AllocTableModel> leftSorter = new javax.swing.table.TableRowSorter<>(leftModel); leftTable.setRowSorter(leftSorter); // Рендерер для колонки «Объём» (индекс 1) — показывает форматированные байты leftTable.getColumnModel().getColumn(1).setCellRenderer(new BytesRenderer()); // Сортировка по объёму (колонка 1) по убыванию leftSorter.setSortKeys(List.of(new javax.swing.RowSorter.SortKey(1, javax.swing.SortOrder.DESCENDING))); leftSorter.sort(); // === Правая таблица: стеки для выбранного класса === AllocTableModel rightModel = new AllocTableModel(new Object[0][3], new String[]{"\u0421\u0442\u0435\u043a", "\u041e\u0431\u044a\u0451\u043c", "\u0421\u043e\u0431\u044b\u0442\u0438\u0439"}); JTable rightTable = new JTable(rightModel); rightTable.setAutoCreateRowSorter(false); javax.swing.table.TableRowSorter<AllocTableModel> rightSorter = new javax.swing.table.TableRowSorter<>(rightModel); rightTable.setRowSorter(rightSorter); rightTable.getColumnModel().getColumn(1).setCellRenderer(new BytesRenderer()); // === Предварительная группировка стеков по классу === Map<String, Map<String, long[]>> stacksByClass = new LinkedHashMap<>(); for (Sample s : profile.alloc) { String cls = s.allocClass != null ? s.allocClass : "<unknown>"; Map<String, long[]> stackMap = stacksByClass.computeIfAbsent(cls, k -> new LinkedHashMap<>()); // Стек: первые 5 кадров через " ← " int limit = Math.min(5, s.frames.length); StringBuilder sb = new StringBuilder(); for (int i = 0; i < limit; i++) { if (i > 0) sb.append(" \u2190 "); sb.append(s.frames[i]); } String stackKey = sb.toString(); long[] acc = stackMap.computeIfAbsent(stackKey, k -> new long[2]); acc[0] += s.weight; acc[1]++; } // === Обработка выбора строки в левой таблице === leftTable.getSelectionModel().addListSelectionListener(e -> { if (e.getValueIsAdjusting()) return; int viewRow = leftTable.getSelectedRow(); if (viewRow < 0) { rightModel.setData(new Object[0][3]); rightTable.setRowSorter(new javax.swing.table.TableRowSorter<>(rightModel)); return; } int modelRow = leftTable.convertRowIndexToModel(viewRow); String selectedClass = (String) leftModel.getValueAt(modelRow, 0); Map<String, long[]> stackMap = stacksByClass.getOrDefault(selectedClass, Collections.emptyMap()); // Сортировка по объёму убывание List<Map.Entry<String, long[]>> stacksSorted = new ArrayList<>(stackMap.entrySet()); stacksSorted.sort((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0])); Object[][] rightData = new Object[stacksSorted.size()][3]; for (int i = 0; i < stacksSorted.size(); i++) { Map.Entry<String, long[]> entry = stacksSorted.get(i); rightData[i][0] = entry.getKey(); rightData[i][1] = entry.getValue()[0]; rightData[i][2] = entry.getValue()[1]; } rightModel.setData(rightData); javax.swing.table.TableRowSorter<AllocTableModel> newRightSorter = new javax.swing.table.TableRowSorter<>(rightModel); rightTable.setRowSorter(newRightSorter); rightTable.getColumnModel().getColumn(1).setCellRenderer(new BytesRenderer()); newRightSorter.setSortKeys(List.of(new javax.swing.RowSorter.SortKey(1, javax.swing.SortOrder.DESCENDING))); newRightSorter.sort(); }); // === Компоновка === JScrollPane leftScroll = new JScrollPane(leftTable); leftScroll.setBorder(BorderFactory.createTitledBorder("\u041a\u043b\u0430\u0441\u0441\u044b \u043f\u043e \u043e\u0431\u044a\u0451\u043c\u0443 \u0430\u043b\u043b\u043e\u043a\u0430\u0446\u0438\u0439")); JScrollPane rightScroll = new JScrollPane(rightTable); rightScroll.setBorder(BorderFactory.createTitledBorder("\u0421\u0442\u0435\u043a\u0438 \u0430\u043b\u043b\u043e\u043a\u0430\u0446\u0438\u0439")); JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftScroll, rightScroll); split.setDividerLocation(500); split.setResizeWeight(0.4); panel.add(split, BorderLayout.CENTER); return panel; } // ========================= Парсер JFR -> Profile (T2, раздел 3) ========================= @FunctionalInterface interface LongConsumer { void accept(long value); } static Profile load(Path path, LongConsumer progress) throws IOException { if (!Files.exists(path)) { throw new IOException("\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d: " + path); } if (Files.size(path) == 0) { throw new IOException("\u0424\u0430\u0439\u043b \u043f\u0443\u0441\u0442: " + path); } Profile prof = new Profile(); prof.source = path; long eventCount = 0; try (RecordingFile rf = new RecordingFile(path)) { while (rf.hasMoreEvents()) { RecordedEvent e = rf.readEvent(); eventCount++; // Прогресс каждые 10000 событий if (progress != null && eventCount % 10000 == 0) { progress.accept(eventCount); } String typeName = e.getEventType().getName(); // Считаем все типы событий prof.eventCounts.merge(typeName, 1L, Long::sum); // Временные границы Instant ts = e.getStartTime(); if (ts != null) { if (prof.first == null || ts.isBefore(prof.first)) prof.first = ts; if (prof.last == null || ts.isAfter(prof.last)) prof.last = ts; } // Обработка интересующих типов событий switch (typeName) { case "jdk.ExecutionSample": case "jdk.NativeMethodSample": processCpuEvent(e, prof); break; case "jdk.ObjectAllocationSample": processAllocEvent(e, prof, "weight"); break; case "jdk.ObjectAllocationInNewTLAB": case "jdk.ObjectAllocationOutsideTLAB": processAllocEvent(e, prof, "allocationSize"); break; case "jdk.JavaMonitorEnter": case "jdk.ThreadPark": processBlockEvent(e, prof); break; default: break; } } } catch (IOException ex) { throw new IOException("\u041e\u0448\u0438\u0431\u043a\u0430 \u0447\u0442\u0435\u043d\u0438\u044f JFR-\u0444\u0430\u0439\u043b\u0430: " + ex.getMessage(), ex); } // Финальный прогресс if (progress != null) { progress.accept(eventCount); } return prof; } private static String extractThreadName(RecordedEvent e) { // Ловушка 3.3: у ExecutionSample поток в поле sampledThread RecordedThread t = null; if (e.hasField("sampledThread")) { Object v = e.getValue("sampledThread"); if (v instanceof RecordedThread) { t = (RecordedThread) v; } } if (t == null) { t = e.getThread(); } if (t == null) return "<unknown>"; String name = t.getJavaName(); if (name != null) return name; name = t.getOSName(); return name != null ? name : "<unknown>"; } private static String[] extractFrames(RecordedStackTrace st) { if (st == null) return null; List<RecordedFrame> frames = st.getFrames(); if (frames == null || frames.isEmpty()) return null; String[] result = new String[frames.size()]; for (int i = 0; i < frames.size(); i++) { RecordedFrame f = frames.get(i); RecordedMethod m = f.getMethod(); if (m == null) { result[i] = "<unknown>"; } else { String typeName = m.getType() != null ? m.getType().getName() : "<unknown>"; String methodName = m.getName() != null ? m.getName() : "<unknown>"; result[i] = typeName + "." + methodName; } } return result; } private static void processCpuEvent(RecordedEvent e, Profile prof) { String threadName = extractThreadName(e); prof.threads.add(threadName); RecordedStackTrace st = e.getStackTrace(); String[] frames = extractFrames(st); if (frames == null) return; // пропускаем события без стека Sample sample = new Sample(threadName, frames, 1L, null); prof.cpu.add(sample); } private static void processAllocEvent(RecordedEvent e, Profile prof, String weightField) { String threadName = extractThreadName(e); prof.threads.add(threadName); RecordedStackTrace st = e.getStackTrace(); String[] frames = extractFrames(st); if (frames == null) return; long weight = 0; try { if (e.hasField(weightField)) { weight = e.getLong(weightField); } } catch (Exception ignored) { weight = 1; } String allocClass = null; try { if (e.hasField("objectClass")) { RecordedClass rc = e.getClass("objectClass"); if (rc != null) { allocClass = rc.getName(); } } } catch (Exception ignored) {} Sample sample = new Sample(threadName, frames, weight, allocClass); prof.alloc.add(sample); } private static void processBlockEvent(RecordedEvent e, Profile prof) { String threadName = extractThreadName(e); prof.threads.add(threadName); RecordedStackTrace st = e.getStackTrace(); String[] frames = extractFrames(st); if (frames == null) return; long weight = 0; try { Duration d = e.getDuration(); if (d != null) { weight = d.toNanos(); } } catch (Exception ignored) { weight = 1; } Sample sample = new Sample(threadName, frames, weight, null); prof.block.add(sample); } // ========================= Построение деревьев (разделы 4.2-4.4) ========================= /** * Прямое дерево (сверху вниз): обход frames с конца (корень) к началу (лист). */ static Node buildForwardTree(List<Sample> samples) { Node root = new Node("(\u0432\u0441\u0435\u0433\u043e)"); for (Sample s : samples) { long w = s.weight; root.total += w; Node cur = root; // frames[length-1] — корень (Thread.run), frames[0] — лист for (int i = s.frames.length - 1; i >= 0; i--) { String fname = s.frames[i]; cur.sortedCache = null; // инвалидация кэша cur = cur.children.computeIfAbsent(fname, Node::new); cur.total += w; } cur.self += w; } return root; } /** * Обратное дерево (backtraces): обход frames от 0 (лист) к length-1 (корень). * Дети корня — листовые методы. */ static Node buildReverseTree(List<Sample> samples) { Node root = new Node("(\u0432\u0441\u0435\u0433\u043e)"); for (Sample s : samples) { long w = s.weight; root.total += w; Node cur = root; // frames[0] — лист, frames[length-1] — корень for (int i = 0; i < s.frames.length; i++) { String fname = s.frames[i]; cur.sortedCache = null; cur = cur.children.computeIfAbsent(fname, Node::new); cur.total += w; } cur.self += w; } return root; } /** * Подсчёт Self и Total для каждого метода (Hot Methods). * Self — вес сэмплов, где метод = frames[0] (лист). * Total — вес сэмплов, где метод встречается где угодно в стеке. * Один метод в одном стеке считается ОДИН РАЗ (HashSet seen). * * Возвращает Map: имя метода -> Node (self, total заполнены, children не используются). */ static Map<String, Node> computeHotMethods(List<Sample> samples) { Map<String, Node> methods = new LinkedHashMap<>(); Set<String> seen = new HashSet<>(); for (Sample s : samples) { long w = s.weight; seen.clear(); // Self: метод-лист (frames[0]) if (s.frames.length > 0) { String leaf = s.frames[0]; methods.computeIfAbsent(leaf, Node::new).self += w; } // Total: каждый уникальный метод в стеке for (String frame : s.frames) { if (seen.add(frame)) { methods.computeIfAbsent(frame, Node::new).total += w; } } } return methods; } // ========================= Текстовый режим (T9 — заглушка) ========================= private static void runTextMode(Path path) { try { Profile prof = load(path, count -> { if (count % 50000 == 0) { System.err.println("\u041f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u0439: " + count); } }); String report = generateTextReport(prof, topN, threadFilter); System.out.println(report); } catch (IOException e) { System.err.println("\u041e\u0448\u0438\u0431\u043a\u0430: " + e.getMessage()); System.exit(1); } } static String generateTextReport(Profile prof, int top, String threadNameFilter) { StringBuilder sb = new StringBuilder(); sb.append("=== JFR \u041e\u0442\u0447\u0451\u0442 ===\n"); sb.append("\u0424\u0430\u0439\u043b: ").append(prof.source).append("\n"); if (prof.first != null && prof.last != null) { sb.append("\u041f\u0435\u0440\u0438\u043e\u0434: ").append(prof.first).append(" \u2014 ").append(prof.last).append("\n"); Duration dur = Duration.between(prof.first, prof.last); sb.append("\u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c: ").append(dur.toSeconds()).append(" \u0441\u0435\u043a\n"); } sb.append("\u041f\u043e\u0442\u043e\u043a\u043e\u0432: ").append(prof.threads.size()).append("\n"); sb.append("CPU-\u0441\u044d\u043c\u043f\u043b\u043e\u0432: ").append(prof.cpu.size()).append("\n"); sb.append("\u0421\u043e\u0431\u044b\u0442\u0438\u0439 \u0430\u043b\u043b\u043e\u043a\u0430\u0446\u0438\u0439: ").append(prof.alloc.size()).append("\n"); sb.append("\u0421\u043e\u0431\u044b\u0442\u0438\u0439 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043e\u043a: ").append(prof.block.size()).append("\n\n"); // Типы событий sb.append("--- \u0422\u0438\u043f\u044b \u0441\u043e\u0431\u044b\u0442\u0438\u0439 ---\n"); for (Map.Entry<String, Long> entry : prof.eventCounts.entrySet()) { sb.append(String.format(" %-50s %,d\n", entry.getKey(), entry.getValue())); } sb.append("\n"); // Фильтрация по потоку List<Sample> cpuFiltered = filterByThread(prof.cpu, threadNameFilter); List<Sample> allocFiltered = filterByThread(prof.alloc, threadNameFilter); // Горячие методы (CPU) по Self Map<String, Node> hotMethods = computeHotMethods(cpuFiltered); List<Map.Entry<String, Node>> bySelf = new ArrayList<>(hotMethods.entrySet()); bySelf.sort((a, b) -> Long.compare(b.getValue().self, a.getValue().self)); long totalSamples = cpuFiltered.stream().mapToLong(s -> s.weight).sum(); sb.append("--- \u0422\u043e\u043f \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u043f\u043e Self (CPU) ---\n"); int count = 0; for (Map.Entry<String, Node> entry : bySelf) { if (count >= top) break; Node n = entry.getValue(); if (n.self == 0) break; double selfPct = totalSamples > 0 ? 100.0 * n.self / totalSamples : 0; double totalPct = totalSamples > 0 ? 100.0 * n.total / totalSamples : 0; sb.append(String.format(" Self: %,8d (%5.1f%%) Total: %,8d (%5.1f%%) %s\n", n.self, selfPct, n.total, totalPct, entry.getKey())); count++; } sb.append("\n"); // Топ по Total List<Map.Entry<String, Node>> byTotal = new ArrayList<>(hotMethods.entrySet()); byTotal.sort((a, b) -> Long.compare(b.getValue().total, a.getValue().total)); sb.append("--- \u0422\u043e\u043f \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u043f\u043e Total (CPU) ---\n"); count = 0; for (Map.Entry<String, Node> entry : byTotal) { if (count >= top) break; Node n = entry.getValue(); if (n.total == 0) break; double selfPct = totalSamples > 0 ? 100.0 * n.self / totalSamples : 0; double totalPct = totalSamples > 0 ? 100.0 * n.total / totalSamples : 0; sb.append(String.format(" Total: %,8d (%5.1f%%) Self: %,8d (%5.1f%%) %s\n", n.total, totalPct, n.self, selfPct, entry.getKey())); count++; } sb.append("\n"); // Топ аллокаций по классам if (!allocFiltered.isEmpty()) { Map<String, long[]> allocByClass = new LinkedHashMap<>(); for (Sample s : allocFiltered) { if (s.allocClass != null) { long[] acc = allocByClass.computeIfAbsent(s.allocClass, k -> new long[2]); acc[0] += s.weight; // байты acc[1]++; // количество } } List<Map.Entry<String, long[]>> allocSorted = new ArrayList<>(allocByClass.entrySet()); allocSorted.sort((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0])); sb.append("--- \u0422\u043e\u043f \u043a\u043b\u0430\u0441\u0441\u043e\u0432 \u043f\u043e \u0430\u043b\u043b\u043e\u043a\u0430\u0446\u0438\u044f\u043c ---\n"); count = 0; for (Map.Entry<String, long[]> entry : allocSorted) { if (count >= Math.min(top, 20)) break; long bytes = entry.getValue()[0]; long events = entry.getValue()[1]; sb.append(String.format(" %12s %,8d \u0441\u043e\u0431\u044b\u0442\u0438\u0439 %s\n", formatBytes(bytes), events, entry.getKey())); count++; } } return sb.toString(); } private static List<Sample> filterByThread(List<Sample> samples, String threadNameFilter) { if (threadNameFilter == null || threadNameFilter.isEmpty()) return samples; List<Sample> result = new ArrayList<>(); for (Sample s : samples) { if (s.thread != null && s.thread.contains(threadNameFilter)) { result.add(s); } } return result; } static String formatBytes(long bytes) { if (bytes < 1024) return bytes + " B"; if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); if (bytes < 1024L * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024)); return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024)); } // === T5 helper classes === class CallTreeModel implements javax.swing.tree.TreeModel { private final Node root; private final List<javax.swing.event.TreeModelListener> listeners = new ArrayList<>(); CallTreeModel(Node root) { this.root = root; } @Override public Object getRoot() { return root; } @Override public Object getChild(Object parent, int index) { return ((Node) parent).sortedChildren().get(index); } @Override public int getChildCount(Object parent) { return ((Node) parent).sortedChildren().size(); } @Override public boolean isLeaf(Object node) { return ((Node) node).children.isEmpty(); } @Override public void valueForPathChanged(javax.swing.tree.TreePath path, Object newValue) { // Read-only модель — изменения не поддерживаются } @Override public int getIndexOfChild(Object parent, Object child) { if (parent == null || child == null) return -1; List<Node> children = ((Node) parent).sortedChildren(); for (int i = 0; i < children.size(); i++) { if (children.get(i) == child) return i; } return -1; } @Override public void addTreeModelListener(javax.swing.event.TreeModelListener l) { listeners.add(l); } @Override public void removeTreeModelListener(javax.swing.event.TreeModelListener l) { listeners.remove(l); } } class CallTreeCellRenderer extends javax.swing.tree.DefaultTreeCellRenderer { private final long rootTotal; CallTreeCellRenderer(long rootTotal) { this.rootTotal = rootTotal; } @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); if (value instanceof Node node) { double totalPct = rootTotal > 0 ? 100.0 * node.total / rootTotal : 0; double selfPct = rootTotal > 0 ? 100.0 * node.self / rootTotal : 0; String label; if (node.self > 0) { label = String.format("%5.1f%% %s (self %.1f%%)", totalPct, node.name, selfPct); } else { label = String.format("%5.1f%% %s", totalPct, node.name); } setText(label); // Цветовая подсветка горячих узлов if (!sel) { if (totalPct >= 50) { setForeground(new Color(204, 0, 0)); // Тёмно-красный } else if (totalPct >= 20) { setForeground(new Color(204, 102, 0)); // Оранжевый } else if (totalPct >= 5) { setForeground(new Color(0, 0, 153)); // Тёмно-синий } else { setForeground(Color.DARK_GRAY); } } } return this; } } // === T6 helper classes === static class ReverseNodeTreeModel implements javax.swing.tree.TreeModel { private final Node root; private final List<javax.swing.event.TreeModelListener> listeners = new ArrayList<>(); ReverseNodeTreeModel(Node root) { this.root = root; } @Override public Object getRoot() { return root; } @Override public Object getChild(Object parent, int index) { return ((Node) parent).sortedChildren().get(index); } @Override public int getChildCount(Object parent) { return ((Node) parent).sortedChildren().size(); } @Override public boolean isLeaf(Object node) { return ((Node) node).sortedChildren().isEmpty(); } @Override public void valueForPathChanged(javax.swing.tree.TreePath path, Object newValue) { // не редактируемое дерево } @Override public int getIndexOfChild(Object parent, Object child) { if (parent == null || child == null) return -1; List<Node> children = ((Node) parent).sortedChildren(); for (int i = 0; i < children.size(); i++) { if (children.get(i) == child) return i; } return -1; } @Override public void addTreeModelListener(javax.swing.event.TreeModelListener l) { listeners.add(l); } @Override public void removeTreeModelListener(javax.swing.event.TreeModelListener l) { listeners.remove(l); } } static class ReverseNodeTreeRenderer extends javax.swing.tree.DefaultTreeCellRenderer { private final long rootTotal; ReverseNodeTreeRenderer(long rootTotal) { this.rootTotal = rootTotal; } @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); if (value instanceof Node node) { double totalPct = rootTotal > 0 ? 100.0 * node.total / rootTotal : 0; double selfPct = rootTotal > 0 ? 100.0 * node.self / rootTotal : 0; setText(String.format("%.1f%% %s (self %.1f%%)", totalPct, node.name, selfPct)); } return this; } } // === T7 helper classes === /** * Прямоугольник flame graph для hit-testing. */ static class FlameRect { final int x, y, w, h; final Node node; FlameRect(int x, int y, int w, int h, Node node) { this.x = x; this.y = y; this.w = w; this.h = h; this.node = node; } boolean contains(int px, int py) { return px >= x && px < x + w && py >= y && py < y + h; } } /** * Панель отрисовки Flame Graph (icicle, корень сверху). */ class FlamePanel extends JPanel { private static final int ROW = 20; private static final double MIN_WIDTH = 0.6; private Node treeRoot; // полный корень дерева private Node viewRoot; // текущий корень отображения (при зуме) private boolean allocMode; private String highlight = ""; private final Deque<Node> zoomStack = new ArrayDeque<>(); private final List<FlameRect> rects = new ArrayList<>(); private FlameRect hoveredRect = null; private int maxDepth; FlamePanel() { setBackground(Color.WHITE); ToolTipManager.sharedInstance().registerComponent(this); ToolTipManager.sharedInstance().setInitialDelay(150); ToolTipManager.sharedInstance().setDismissDelay(15000); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { FlameRect fr = findRect(e.getX(), e.getY()); if (fr != null && fr.node != viewRoot) { zoomStack.push(viewRoot); viewRoot = fr.node; rebuildView(); } } } }); addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { FlameRect fr = findRect(e.getX(), e.getY()); if (fr != hoveredRect) { hoveredRect = fr; repaint(); } } }); } void setRoot(Node root, boolean alloc) { this.treeRoot = root; this.viewRoot = root; this.allocMode = alloc; zoomStack.clear(); rebuildView(); } void setHighlight(String text) { this.highlight = (text == null) ? "" : text.trim().toLowerCase(); repaint(); } void zoomBack() { if (!zoomStack.isEmpty()) { viewRoot = zoomStack.pop(); rebuildView(); } } void zoomReset() { zoomStack.clear(); viewRoot = treeRoot; rebuildView(); } private void rebuildView() { maxDepth = computeMaxDepth(viewRoot, 0); int h = (maxDepth + 1) * ROW + 4; setPreferredSize(new Dimension(getParent() != null ? getParent().getWidth() : 800, h)); revalidate(); repaint(); } private int computeMaxDepth(Node node, int depth) { int max = depth; for (Node child : node.sortedChildren()) { int d = computeMaxDepth(child, depth + 1); if (d > max) max = d; } return max; } private FlameRect findRect(int px, int py) { for (int i = rects.size() - 1; i >= 0; i--) { FlameRect r = rects.get(i); if (r.contains(px, py)) return r; } return null; } @Override public String getToolTipText(MouseEvent e) { FlameRect fr = findRect(e.getX(), e.getY()); if (fr == null) return null; Node n = fr.node; long rootTotal = viewRoot.total; double pct = rootTotal > 0 ? 100.0 * n.total / rootTotal : 0; String weightStr; if (allocMode) { weightStr = formatBytes(n.total) + " (" + String.format("%.1f%%", pct) + ")"; } else { weightStr = n.total + " \u0441\u044d\u043c\u043f\u043b\u043e\u0432 (" + String.format("%.1f%%", pct) + ")"; } return "<html><b>" + escapeHtml(n.name) + "</b><br>" + weightStr + "</html>"; } private String escapeHtml(String s) { return s.replace("&", "&").replace("<", "<").replace(">", ">"); } @Override protected void paintComponent(Graphics g0) { super.paintComponent(g0); if (viewRoot == null || viewRoot.total == 0) { g0.setColor(Color.GRAY); g0.drawString("\u041d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445", 20, 30); return; } Graphics2D g = (Graphics2D) g0; g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); rects.clear(); int panelWidth = getWidth(); drawNode(g, viewRoot, 0, panelWidth, 0); } private void drawNode(Graphics2D g, Node node, double x, double width, int depth) { if (width < MIN_WIDTH) return; int ix = (int) Math.round(x); int iy = depth * ROW; int iw = Math.max(1, (int) Math.round(x + width) - ix); int ih = ROW - 1; rects.add(new FlameRect(ix, iy, iw, ih, node)); // Цвет Color fill; boolean highlighted = !highlight.isEmpty() && node.name.toLowerCase().contains(highlight); boolean hovered = (hoveredRect != null && hoveredRect.node == node); if (highlighted) { // Синий для подсвеченных int h = Math.abs(node.name.hashCode()); float sat = 0.5f + (h % 30) / 75.0f; float bri = 0.80f + (h % 10) / 66.0f; fill = Color.getHSBColor(0.6f, sat, bri); } else { // Тёплая палитра: hue от 0.0 до 0.33 int h = Math.abs(node.name.hashCode()); float hue = 0.0f + (h % 60) / 180.0f; float sat = 0.6f + (h % 31) / 100.0f; float bri = 0.85f + (h % 11) / 100.0f; fill = Color.getHSBColor(hue, sat, bri); } if (hovered) { fill = fill.brighter(); } g.setColor(fill); g.fillRect(ix, iy, iw, ih); g.setColor(new Color(0, 0, 0, 60)); g.drawRect(ix, iy, iw - 1, ih - 1); // Текст (если помещается) if (iw > 30) { g.setColor(Color.BLACK); Font font = g.getFont(); FontMetrics fm = g.getFontMetrics(font); String label = node.name; int textW = fm.stringWidth(label); if (textW > iw - 4) { // Обрезаем int chars = (iw - 8) / Math.max(1, fm.charWidth('a')); if (chars > 0 && chars < label.length()) { label = label.substring(0, chars) + "\u2026"; } else if (chars <= 0) { label = ""; } } if (!label.isEmpty()) { int textY = iy + (ih + fm.getAscent() - fm.getDescent()) / 2; g.drawString(label, ix + 2, textY); } } // Дети double childX = x; for (Node child : node.sortedChildren()) { double childW = width * child.total / (double) node.total; drawNode(g, child, childX, childW, depth + 1); childX += childW; } } } // === T8 helper classes === /** * Модель таблицы для вкладки «Аллокации». * Поддерживает правильный getColumnClass для сортировки числовых колонок. */ static class AllocTableModel extends javax.swing.table.AbstractTableModel { private Object[][] data; private final String[] columns; AllocTableModel(Object[][] data, String[] columns) { this.data = data; this.columns = columns; } void setData(Object[][] newData) { this.data = newData; fireTableDataChanged(); } @Override public int getRowCount() { return data.length; } @Override public int getColumnCount() { return columns.length; } @Override public String getColumnName(int col) { return columns[col]; } @Override public Class<?> getColumnClass(int col) { if (col == 0) return String.class; return Long.class; } @Override public Object getValueAt(int row, int col) { return data[row][col]; } @Override public boolean isCellEditable(int row, int col) { return false; } } /** * Рендерер для колонки «Объём»: показывает человекочитаемый формат (KB, MB, GB), * но сортировка идёт по сырому long-значению из модели. */ static class BytesRenderer extends javax.swing.table.DefaultTableCellRenderer { BytesRenderer() { setHorizontalAlignment(SwingConstants.RIGHT); } @Override protected void setValue(Object value) { if (value instanceof Long) { setText(JfrViewer.formatBytes((Long) value)); } else { super.setValue(value); } } } }