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 872e73ed1e)
This commit is contained in:
Maxim Kartashev
2024-04-25 17:25:31 +04:00
parent 024377b618
commit 94de2224fa
4 changed files with 215 additions and 0 deletions

View File

@@ -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<String, PerfCounter> perfCounters = new HashMap<>(4);
private transient final Map<String, PerfCounter> perfCountersPrev = new HashMap<>(4);
} // class Window

View File

@@ -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<Component,Rectangle> 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()))

View File

@@ -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);
}
/**

View File

@@ -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();
});
}
}