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
+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" );