From 94de2224fa877859bd695205dd30be0d60d68d20 Mon Sep 17 00:00:00 2001 From: Maxim Kartashev Date: Thu, 25 Apr 2024 17:25:31 +0400 Subject: [PATCH] JBR-7028 Implement FPS counter on Linux Use -Dawt.window.counters to enable. To output counters per second to stdout/stderr, use -Dawt.window.counters=stdout or =stderr. A counter by the name swing.RepaintManager.updateWindows is always available for Swing applications, but it does not accurately correspond to frames per second. Toolkit-dependent counters provide much better accuracy. On Wayland with memory buffers as the backend two are available: java2d.native.frames - frames delivered to the Wayland server java2d.native.framesDropped - fully formed frames that were not delivered to the Wayland server (cherry picked from commit 872e73ed1e76377b30c7cd9e5da140a7ef1dc54f) --- .../share/classes/java/awt/Window.java | 94 +++++++++++++++ .../classes/javax/swing/RepaintManager.java | 8 ++ .../share/classes/sun/awt/AWTAccessor.java | 5 + .../awt/Counters/UpdateWindowsCounter.java | 108 ++++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 test/jdk/jb/java/awt/Counters/UpdateWindowsCounter.java diff --git a/src/java.desktop/share/classes/java/awt/Window.java b/src/java.desktop/share/classes/java/awt/Window.java index f981202a55bb..fbc61a87e356 100644 --- a/src/java.desktop/share/classes/java/awt/Window.java +++ b/src/java.desktop/share/classes/java/awt/Window.java @@ -389,6 +389,7 @@ public class Window extends Container implements Accessible { private static final PlatformLogger log = PlatformLogger.getLogger("java.awt.Window"); private static final PlatformLogger focusRequestLog = PlatformLogger.getLogger("jb.focus.requests"); + private static final PlatformLogger perfLog = PlatformLogger.getLogger("awt.window.counters"); private static final boolean locationByPlatformProp; @@ -4276,6 +4277,8 @@ public class Window extends Container implements Accessible { } static { + String counters = System.getProperty("awt.window.counters"); + AWTAccessor.setWindowAccessor(new AWTAccessor.WindowAccessor() { public void updateWindow(Window window) { window.updateWindow(); @@ -4300,6 +4303,92 @@ public class Window extends Container implements Accessible { public Window[] getOwnedWindows(Window w) { return w.getOwnedWindows_NoClientCode(); } + + public boolean countersEnabled(Window w) { + // May want to selectively enable or disable counters per window + return counters != null; + } + + public void bumpCounter(Window w, String counterName) { + Objects.requireNonNull(w); + Objects.requireNonNull(counterName); + + PerfCounter newCounter; + long curTimeNanos = System.nanoTime(); + synchronized (w.perfCounters) { + newCounter = w.perfCounters.compute(counterName, (k, v) -> + v == null + ? new PerfCounter(curTimeNanos, 1L) + : new PerfCounter(curTimeNanos, v.value + 1)); + } + PerfCounter prevCounter; + synchronized (w.perfCountersPrev) { + prevCounter = w.perfCountersPrev.putIfAbsent(counterName, newCounter); + } + if (prevCounter != null) { + long nanosInSecond = java.util.concurrent.TimeUnit.SECONDS.toNanos(1); + long timeDeltaNanos = curTimeNanos - prevCounter.updateTimeNanos; + if (timeDeltaNanos > nanosInSecond) { + long valPerSecond = (long) ((double) (newCounter.value - prevCounter.value) + * nanosInSecond / timeDeltaNanos); + boolean traceAllCounters = Objects.equals(counters, "") + || Objects.equals(counters, "stdout") + || Objects.equals(counters, "stderr"); + boolean traceEnabled = traceAllCounters || (counters != null && counters.contains(counterName)); + if (traceEnabled) { + if (counters.contains("stderr")) { + System.err.println(counterName + " per second: " + valPerSecond); + } else { + System.out.println(counterName + " per second: " + valPerSecond); + } + } + if (perfLog.isLoggable(PlatformLogger.Level.FINE)) { + perfLog.fine(counterName + " per second: " + valPerSecond); + } + synchronized (w.perfCountersPrev) { + w.perfCountersPrev.put(counterName, newCounter); + } + } + } + } + + public long getCounter(Window w, String counterName) { + Objects.requireNonNull(w); + Objects.requireNonNull(counterName); + + synchronized (w.perfCounters) { + PerfCounter counter = w.perfCounters.get(counterName); + return counter != null ? counter.value : -1L; + } + } + + public long getCounterPerSecond(Window w, String counterName) { + Objects.requireNonNull(w); + Objects.requireNonNull(counterName); + + PerfCounter newCounter; + PerfCounter prevCounter; + + synchronized (w.perfCounters) { + newCounter = w.perfCounters.get(counterName); + } + + synchronized (w.perfCountersPrev) { + prevCounter = w.perfCountersPrev.get(counterName); + } + + if (newCounter != null && prevCounter != null) { + long timeDeltaNanos = newCounter.updateTimeNanos - prevCounter.updateTimeNanos; + // Note that this time delta will usually be above one second. + if (timeDeltaNanos > 0) { + long nanosInSecond = java.util.concurrent.TimeUnit.SECONDS.toNanos(1); + long valPerSecond = (long) ((double) (newCounter.value - prevCounter.value) + * nanosInSecond / timeDeltaNanos); + return valPerSecond; + } + } + return -1; + } }); // WindowAccessor } // static @@ -4307,6 +4396,11 @@ public class Window extends Container implements Accessible { @Override void updateZOrder() {} + private record PerfCounter(Long updateTimeNanos, Long value) {} + + private transient final Map perfCounters = new HashMap<>(4); + private transient final Map perfCountersPrev = new HashMap<>(4); + } // class Window diff --git a/src/java.desktop/share/classes/javax/swing/RepaintManager.java b/src/java.desktop/share/classes/javax/swing/RepaintManager.java index 40dddb471a2f..11ca83a01892 100644 --- a/src/java.desktop/share/classes/javax/swing/RepaintManager.java +++ b/src/java.desktop/share/classes/javax/swing/RepaintManager.java @@ -47,6 +47,7 @@ import sun.java2d.pipe.Region; import sun.swing.SwingAccessor; import sun.swing.SwingUtilities2; import sun.swing.SwingUtilities2.RepaintListener; +import java.util.stream.Collectors; /** * This class manages repaint requests, allowing the number @@ -715,6 +716,13 @@ public class RepaintManager } private void updateWindows(Map dirtyComponents) { + dirtyComponents.keySet().stream() + .map(c -> c instanceof Window w ? w : SwingUtilities.getWindowAncestor(c)) + .filter(Objects::nonNull) + .distinct() + .forEach(w -> AWTAccessor.getWindowAccessor() + .bumpCounter(w, "swing.RepaintManager.updateWindows")); + Toolkit toolkit = Toolkit.getDefaultToolkit(); if (!(toolkit instanceof SunToolkit && ((SunToolkit)toolkit).needUpdateWindow())) diff --git a/src/java.desktop/share/classes/sun/awt/AWTAccessor.java b/src/java.desktop/share/classes/sun/awt/AWTAccessor.java index 65a68010602b..a4b87c3a74ac 100644 --- a/src/java.desktop/share/classes/sun/awt/AWTAccessor.java +++ b/src/java.desktop/share/classes/sun/awt/AWTAccessor.java @@ -324,6 +324,11 @@ public final class AWTAccessor { * window currently owns. */ Window[] getOwnedWindows(Window w); + + boolean countersEnabled(Window w); + void bumpCounter(Window w, String counterName); + long getCounter(Window w, String counterName); + long getCounterPerSecond(Window w, String counterName); } /** diff --git a/test/jdk/jb/java/awt/Counters/UpdateWindowsCounter.java b/test/jdk/jb/java/awt/Counters/UpdateWindowsCounter.java new file mode 100644 index 000000000000..cc36d8bfb783 --- /dev/null +++ b/test/jdk/jb/java/awt/Counters/UpdateWindowsCounter.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024 JetBrains s.r.o. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +import jdk.test.lib.process.ProcessTools; +import jdk.test.lib.process.OutputAnalyzer; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * @test + * @summary Verifies that swing.RepaintManager.updateWindows performance counter gets + * updated for a Swing application + * @key headful + * @library /test/lib + * @run main UpdateWindowsCounter + */ + +public class UpdateWindowsCounter { + private static int counter = 25; + + public static void main(String[] args) throws Exception { + if (args.length > 0) { + runSwingApp(); + } else { + runOneTest("-Dawt.window.counters=stdout", "swing.RepaintManager.updateWindows per second"); + runOneTest("-Dawt.window.counters=swing.RepaintManager.updateWindows,stdout", "swing.RepaintManager.updateWindows per second"); + runOneTest("-Dawt.window.counters=stderr,swing.RepaintManager.updateWindows", "swing.RepaintManager.updateWindows per second"); + runOneTest("-Dawt.window.counters", "swing.RepaintManager.updateWindows per second"); + runOneTest("-Dawt.window.counters=", "swing.RepaintManager.updateWindows per second"); + runOneTest("-Dawt.window.counters=swing.RepaintManager.updateWindows", "swing.RepaintManager.updateWindows per second"); + + runOneNegativeTest("", "swing.RepaintManager.updateWindows"); + runOneNegativeTest("-Dawt.window.counters=UNCOUNTABLE", "swing.RepaintManager.updateWindows"); + } + } + + private static void runOneTest(String vmArg, String expectedOutput) throws Exception { + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(vmArg, + UpdateWindowsCounter.class.getName(), + "test"); + + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + output.shouldContain(expectedOutput); + } + + private static void runOneNegativeTest(String vmArg, String unexpectedOutput) throws Exception { + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(vmArg, + UpdateWindowsCounter.class.getName(), + "test"); + + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + output.shouldNotContain(unexpectedOutput); + } + + private static void runSwingApp() { + SwingUtilities.invokeLater(() -> { + JFrame frame = new JFrame("Timer App"); + frame.setSize(300, 200); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + JLabel label = new JLabel("---", SwingConstants.CENTER); + frame.getContentPane().add(label); + + Timer timer = new Timer(100, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + counter--; + label.setText(String.valueOf(counter)); + + if (counter == 0) { + ((Timer) e.getSource()).stop(); + frame.dispose(); + } + } + }); + + frame.setVisible(true); + timer.start(); + }); + } +} \ No newline at end of file