From 2d39d6130625d453284c0faa2c06a712aa7141aa Mon Sep 17 00:00:00 2001 From: Sergei Tachenov Date: Wed, 19 Mar 2025 13:43:41 +0200 Subject: [PATCH] [terminal] IJPL-182482 Add repaint / paint callbacks to EditorImpl These are low-level hacky hooks that are needed to track repaint requests and the following paint operations to measure terminal frontend output latency. With these hooks we can try to measure when the output is actually painted. GitOrigin-RevId: d308e50ebffd0705251c3d560e2979d9ff8a7f48 --- .../editor/impl/EditorComponentImpl.java | 9 +++++ .../openapi/editor/impl/EditorImpl.java | 34 +++++++++++++++++++ .../openapi/editor/impl/view/EditorView.java | 14 ++++++++ 3 files changed, 57 insertions(+) diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorComponentImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorComponentImpl.java index 175fc949f013..cc78f05bc2e6 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorComponentImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorComponentImpl.java @@ -81,6 +81,8 @@ public final class EditorComponentImpl extends JTextComponent implements Scrolla private final EditorImpl editor; + private @Nullable Runnable myRepaintCallback; + public EditorComponentImpl(@NotNull EditorImpl editor) { this.editor = editor; enableEvents(AWTEvent.KEY_EVENT_MASK | AWTEvent.INPUT_METHOD_EVENT_MASK); @@ -136,6 +138,10 @@ public final class EditorComponentImpl extends JTextComponent implements Scrolla setupEditorSwingCaretUpdatesCourierIfRequired(); } + void setRepaintCallback(@Nullable Runnable repaintCallback) { + myRepaintCallback = repaintCallback; + } + @Override public void uiSettingsChanged(@NotNull UISettings uiSettings) { UISettingsUtils settingsUtils = UISettingsUtils.with(uiSettings); @@ -282,6 +288,9 @@ public final class EditorComponentImpl extends JTextComponent implements Scrolla int topOverhang = Math.max(0, editor.myView.getTopOverhang()); int bottomOverhang = Math.max(0, editor.myView.getBottomOverhang()); repaint(x, y - topOverhang, width, height + topOverhang + bottomOverhang); + if (myRepaintCallback != null && isShowing() && width > 0 && height > 0) { + myRepaintCallback.run(); + } } //--implementation of Scrollable interface-------------------------------------- diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java index a96800e70606..bff433841a8a 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java @@ -2038,6 +2038,40 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi LOG.assertTrue(success || isReleased); } + /** + * Sets a callback invoked after every repaint request. + *

+ * The callback is only invoked if the component is showing and the request has a non-empty rectangle. + * In other words, it's invoked when the repaint request is likely to be followed by an actual painting operation. + *

+ *

+ * The callback must be fast, non-intrusive, and should not throw any exceptions. + *

+ *

+ * This is an internal hack, which is why it doesn't follow the usual add-listener pattern. + *

+ * @param callback the callback, {@code null} removes the callback + */ + @ApiStatus.Internal + public void setRepaintCallback(@Nullable Runnable callback) { + myEditorComponent.setRepaintCallback(callback); + } + + /** + * Sets a callback invoked after every paint operation. + *

+ * The callback must be fast, non-intrusive, and should not throw any exceptions. + *

+ *

+ * This is an internal hack, which is why it doesn't follow the usual add-listener pattern. + *

+ * @param callback the callback, {@code null} removes the callback + */ + @ApiStatus.Internal + public void setPaintCallback(@Nullable Runnable callback) { + myView.setPaintCallback(callback); + } + @Override public boolean isStickySelection() { return myState.isStickySelection(); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java index 86002d509273..c74e0dc10f0d 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java @@ -24,6 +24,7 @@ import com.intellij.util.concurrency.annotations.RequiresEdt; import org.intellij.lang.annotations.JdkConstants; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.awt.*; @@ -75,6 +76,8 @@ public final class EditorView implements TextDrawingCallback, Disposable, Dumpab private final Object myLock = new Object(); + private @Nullable Runnable myPaintCallback; + public EditorView(@NotNull EditorImpl editor) { this(editor, editor.getEditorModel()); } @@ -99,6 +102,14 @@ public final class EditorView implements TextDrawingCallback, Disposable, Dumpab Disposer.register(this, mySizeManager); } + /** + * @see EditorImpl#setPaintCallback(Runnable) + */ + @ApiStatus.Internal + public void setPaintCallback(@Nullable Runnable paintCallback) { + myPaintCallback = paintCallback; + } + @RequiresEdt public int yToVisualLine(int y) { assertNotInBulkMode(); @@ -211,6 +222,9 @@ public final class EditorView implements TextDrawingCallback, Disposable, Dumpab getSoftWrapModel().prepareToMapping(); checkFontRenderContext(g.getFontRenderContext()); myPainter.paint(g); + if (myPaintCallback != null) { + myPaintCallback.run(); + } } @RequiresEdt