Optimize bulk document insertion and IME location updates

Build multiline TextDocumentLine values once and move-insert them as a range, while batching client notifications under one lock.

Queue InputMethod location changes globally and flush only the latest rectangle at a configurable 50 ms interval. Keep backend calls on the main thread and avoid requests from unfocused text widgets. Use EEPP_IME_LOCATION_UPDATE_INTERVAL_MS to override the interval; 0 flushes once per rendered frame.

Add large multiline insertion coverage and document the remaining NewMultiFileDiffViewer performance work.

Enable Haiku x86_64 nightly builds
This commit is contained in:
Martín Lucas Golini
2026-08-29 22:21:55 -03:00
parent 7dd21fa77c
commit caba2d4462
11 changed files with 171 additions and 54 deletions
+34 -34
View File
@@ -443,40 +443,40 @@ jobs:
files: |
projects/freebsd/ecode/ecode-freebsd-${{ env.INSTALL_REF }}-x86_64.tar.gz
# build_haiku_x86_64:
# name: Haiku x86_64 Nightly
# needs: release
# runs-on: ubuntu-latest
# steps:
# - name: Checkout Code
# uses: actions/checkout@v6
# with: { fetch-depth: 0, submodules: 'recursive' }
# - name: Set Environment Variables
# run: |
# echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV"
# echo "RARCH=$(uname -m)" >> "$GITHUB_ENV"
# - uses: vmactions/haiku-vm@v1
# env:
# INSTALL_REF: ${{ needs.release.outputs.version }}
# with:
# envs: 'INSTALL_REF'
# usesh: true
# mem: 8192
# disable-cache: true
# prepare: |
# pkgman install -y libsdl2 libsdl2_devel git gcc premake5
# run: |
# git config --global --add safe.directory "$GITHUB_WORKSPACE"
# bash projects/scripts/patch_commit_number.sh
# sh projects/haiku/ecode/build.app.sh --version ${{ env.INSTALL_REF }}
# - name: Upload Files
# uses: softprops/action-gh-release@v3
# with:
# tag_name: ${{ needs.release.outputs.version }}
# draft: false
# prerelease: true
# files: |
# projects/haiku/ecode/ecode-haiku-${{ env.INSTALL_REF }}-x86_64.tar.gz
build_haiku_x86_64:
name: Haiku x86_64 Nightly
needs: release
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
with: { fetch-depth: 0, submodules: 'recursive' }
- name: Set Environment Variables
run: |
echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV"
echo "RARCH=$(uname -m)" >> "$GITHUB_ENV"
- uses: vmactions/haiku-vm@v1
env:
INSTALL_REF: ${{ needs.release.outputs.version }}
with:
envs: 'INSTALL_REF'
usesh: true
mem: 8192
disable-cache: true
prepare: |
pkgman install -y libsdl2 libsdl2_devel git gcc premake5
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
bash projects/scripts/patch_commit_number.sh
sh projects/haiku/ecode/build.app.sh --version ${{ env.INSTALL_REF }}
- name: Upload Files
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.release.outputs.version }}
draft: false
prerelease: true
files: |
projects/haiku/ecode/ecode-haiku-${{ env.INSTALL_REF }}-x86_64.tar.gz
build_windows_msvc:
name: Windows x86_64 MSVC Nightly
+2
View File
@@ -853,6 +853,8 @@ class EE_API TextDocument {
void notifyLineChanged( const Int64& lineIndex );
void notifyLinesChanged( const Int64& firstLine, const Int64& lastLine );
void notifyUndoRedo( const UndoRedo& eventType );
void notifyDirtyOnFileSystem();
+17
View File
@@ -17,6 +17,23 @@ class EE_API TextDocumentLine {
updateState();
}
TextDocumentLine( String&& text, std::shared_ptr<Mutex> docMutex ) :
mText( std::move( text ) ), mDocMutex( std::move( docMutex ) ) {
updateState();
}
TextDocumentLine( const TextDocumentLine& ) = default;
TextDocumentLine( TextDocumentLine&& other ) noexcept : mDocMutex( other.mDocMutex ) {
ConditionalLock lock( mDocMutex != nullptr, mDocMutex.get() );
mText = std::move( other.mText );
mHash = other.mHash;
mFlags = other.mFlags;
other.mDocMutex.reset();
}
TextDocumentLine& operator=( const TextDocumentLine& ) = default;
~TextDocumentLine() {
if ( mDocMutex ) {
// Wait for any readers to finish before destruction
+10
View File
@@ -23,6 +23,12 @@ class EE_API InputMethod {
Int32 length{ 0 };
};
/**
* Queues the text-input candidate location. Backend updates are coalesced globally and limited
* to one every 50 ms by default. Set EEPP_IME_LOCATION_UPDATE_INTERVAL_MS to a non-negative
* integer to override the interval (values are capped at 65535 ms); 0 applies the latest
* location once per rendered frame.
*/
void setLocation( Rect rect );
bool isEditing() const;
@@ -51,11 +57,15 @@ class EE_API InputMethod {
EE::Window::Window* mWindow{ nullptr };
InputMethod::State mState;
bool mEditing{ false };
bool mLocationDirty{ false };
Uint16 mLocationUpdateCooldown{ 0 };
Rect mLastLocation;
Uint32 mLastCb{ 0 };
std::map<Uint32, TextEditingCb> mEditingCbs;
void sendTextEditing( const String& txt, Int32 start, Int32 length );
void updateLocation();
};
}} // namespace EE::Window
+42 -15
View File
@@ -16,6 +16,8 @@
#include <eepp/ui/doc/textdocument.hpp>
#include <eepp/window/engine.hpp>
#include <algorithm>
#include <iterator>
#include <set>
using namespace std::literals;
@@ -1570,30 +1572,47 @@ TextPosition TextDocument::insert( const size_t& cursorIdx, TextPosition positio
position = sanitizePosition( position );
size_t lineCount = linesCount();
Int64 linesAdd = 0;
bool multiline = text.find( '\n' ) != String::InvalidPos;
{
Lock l( mLinesMutex );
if ( text.find( '\n' ) == String::InvalidPos ) {
if ( !multiline ) {
mLines[position.line()].insert( position.column(), text );
notifyLineChanged( position.line() );
} else {
String before = mLines[position.line()].substr( 0, position.column() );
String after = mLines[position.line()].substr( position.column() );
std::vector<String> lines = text.split( '\n', true );
linesAdd = eemax<Int64>( 0, static_cast<Int64>( lines.size() ) - 1 );
for ( auto i = 0; i < linesAdd; i++ )
lines[i] = lines[i] + "\n";
lines[0] = before + lines[0];
lines[lines.size() - 1] = lines[lines.size() - 1] + after;
{
Lock l2( *mDocumentMutex );
String before = mLines[position.line()].substr( 0, position.column() );
String after = mLines[position.line()].substr( position.column() );
String::View textView = text.view();
linesAdd =
static_cast<Int64>( std::count( textView.begin(), textView.end(), '\n' ) );
// Build the new document lines once and insert the complete range. Inserting each
// line separately repeatedly grows the vector and shifts its trailing elements.
SmallVector<TextDocumentLine, 8> lines;
lines.reserve( static_cast<size_t>( linesAdd ) );
mLines[position.line()] = TextDocumentLine( lines[0], mDocumentMutex );
notifyLineChanged( position.line() );
size_t lineStart = 0;
for ( Int64 i = 0; i <= linesAdd; ++i ) {
size_t newLine = textView.find( '\n', lineStart );
size_t lineEnd = newLine == String::InvalidPos ? textView.size() : newLine + 1;
String line( textView.substr( lineStart, lineEnd - lineStart ) );
if ( i == 0 )
line.insert( 0, before );
if ( i == linesAdd )
line.append( after );
if ( i == 0 )
mLines[position.line()].setText( std::move( line ) );
else
lines.emplace_back( std::move( line ), mDocumentMutex );
lineStart = lineEnd;
}
for ( Int64 i = 1; i < (Int64)lines.size(); i++ ) {
mLines.insert( mLines.begin() + position.line() + i,
TextDocumentLine( lines[i], mDocumentMutex ) );
notifyLineChanged( position.line() + i );
mLines.insert( mLines.begin() + position.line() + 1,
std::make_move_iterator( lines.begin() ),
std::make_move_iterator( lines.end() ) );
}
notifyLinesChanged( position.line(), position.line() + linesAdd );
}
}
@@ -4398,6 +4417,14 @@ void TextDocument::notifyLineChanged( const Int64& lineIndex ) {
}
}
void TextDocument::notifyLinesChanged( const Int64& firstLine, const Int64& lastLine ) {
Lock l( mClientsMutex );
for ( Int64 lineIndex = firstLine; lineIndex <= lastLine; ++lineIndex ) {
for ( auto& client : mClients )
client->onDocumentLineChanged( lineIndex );
}
}
void TextDocument::notifyUndoRedo( const TextDocument::UndoRedo& eventType ) {
Lock l( mClientsMutex );
for ( auto& client : mClients ) {
+2 -2
View File
@@ -1156,8 +1156,8 @@ Uint32 UICodeEditor::onTextInput( const TextInputEvent& event ) {
}
void UICodeEditor::updateIMELocation() {
if ( mDoc->isLoading() || mDoc->getActiveClient() != this || !Engine::isMainThread() ||
mDocView.isFolded( mDoc->getSelection( true ).start().line() ) )
if ( !hasFocus() || mDoc->isLoading() || mDoc->getActiveClient() != this ||
!Engine::isMainThread() || mDocView.isFolded( mDoc->getSelection( true ).start().line() ) )
return;
Rectf r( getScreenPosition( mDoc->getSelection( true ).start() ) );
getUISceneNode()->getWindow()->getIME().setLocation( r.asInt() );
+1 -1
View File
@@ -1051,7 +1051,7 @@ Uint32 UIConsole::onTextEditing( const TextEditingEvent& event ) {
}
void UIConsole::updateIMELocation( const Rectf& loc ) {
if ( mDoc.getActiveClient() != this )
if ( !hasFocus() || mDoc.getActiveClient() != this )
return;
getUISceneNode()->getWindow()->getIME().setLocation( loc.asInt() );
}
+1 -1
View File
@@ -941,7 +941,7 @@ Uint32 UITextInput::onTextInput( const TextInputEvent& event ) {
}
void UITextInput::updateIMELocation() {
if ( mDoc.getActiveClient() != this || !Engine::isMainThread() )
if ( !hasFocus() || mDoc.getActiveClient() != this || !Engine::isMainThread() )
return;
Vector2f cursor( eefloor( mScreenPos.x + mRealAlignOffset.x + mCurPos.x + mPaddingPx.Left ),
mScreenPos.y + mRealAlignOffset.y + mCurPos.y + mPaddingPx.Top );
+40 -1
View File
@@ -1,10 +1,28 @@
#include <eepp/graphics/primitives.hpp>
#include <eepp/graphics/text.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/window/inputmethod.hpp>
#include <eepp/window/window.hpp>
#include <algorithm>
#include <limits>
namespace EE { namespace Window {
static constexpr Uint32 DEFAULT_LOCATION_UPDATE_INTERVAL_MS = 50;
static Uint16 getLocationUpdateInterval() {
static Uint16 interval = [] {
std::string value = Sys::getEnv( "EEPP_IME_LOCATION_UPDATE_INTERVAL_MS" );
Uint32 parsedValue;
if ( value.empty() || !String::fromString( parsedValue, value ) )
return static_cast<Uint16>( DEFAULT_LOCATION_UPDATE_INTERVAL_MS );
return static_cast<Uint16>(
std::min<Uint32>( parsedValue, std::numeric_limits<Uint16>::max() ) );
}();
return interval;
}
InputMethod::InputMethod( EE::Window::Window* window ) : mWindow( window ) {}
void InputMethod::sendTextEditing( const String& txt, Int32 start, Int32 length ) {
@@ -17,11 +35,32 @@ void InputMethod::setLocation( Rect rect ) {
rect = PixelDensity::pxToDpI( rect );
#endif
if ( rect != mLastLocation ) {
mWindow->setTextInputRect( rect );
mLastLocation = std::move( rect );
mLocationDirty = true;
}
}
void InputMethod::updateLocation() {
double elapsed = mWindow->getElapsed().asMilliseconds();
if ( elapsed >= mLocationUpdateCooldown )
mLocationUpdateCooldown = 0;
else
mLocationUpdateCooldown -= static_cast<Uint16>( elapsed );
if ( !mLocationDirty )
return;
if ( mLocationUpdateCooldown > 0 )
return;
// SDL3 can synchronously notify the platform IME (including a DBus round-trip on Linux).
// Apply only the latest requested location at a bounded rate and keep the backend call on the
// main thread, as required by SDL.
mLocationDirty = false;
mLocationUpdateCooldown = getLocationUpdateInterval();
mWindow->setTextInputRect( mLastLocation );
}
bool InputMethod::isEditing() const {
return mEditing;
}
+1
View File
@@ -463,6 +463,7 @@ void Window::display( bool clear ) {
GlobalBatchRenderer::instance()->draw();
if ( TextureFactory* textureFactory = TextureFactory::existsSingleton() )
textureFactory->collectReleasedTextures();
mIME.updateLocation();
swapBuffers();
@@ -127,6 +127,27 @@ UTEST( TextDocument, insertIntoExistingLines ) {
EXPECT_STRINGEQ( "Yond\n", doc.line( 3 ).getText() );
}
UTEST( TextDocument, insertLargeMultilineBlock ) {
constexpr size_t insertedLineCount = 8192;
String text;
text.reserve( insertedLineCount * 4 + 4 );
for ( size_t i = 0; i < insertedLineCount; ++i )
text.append( "row\n" );
text.append( "last" );
TextDocument doc;
doc.insert( 0, { 0, 0 }, "head\nmiddle\ntail" );
TextPosition cursor = doc.insert( 0, { 1, 3 }, text );
EXPECT_EQ( insertedLineCount + 1, static_cast<size_t>( cursor.line() ) );
EXPECT_EQ( 4, cursor.column() );
EXPECT_EQ( insertedLineCount + 3, doc.linesCount() );
EXPECT_STRINGEQ( "midrow\n", doc.line( 1 ).getText() );
EXPECT_STRINGEQ( "row\n", doc.line( insertedLineCount ).getText() );
EXPECT_STRINGEQ( "lastdle\n", doc.line( insertedLineCount + 1 ).getText() );
EXPECT_STRINGEQ( "tail\n", doc.line( insertedLineCount + 2 ).getText() );
}
UTEST( TextDocument, insertEmptyTextDoesNothing ) {
TextDocument doc;
doc.insert( 0, { 0, 0 }, "content" );