perf: reduce allocations across UI and editor hot paths

- reuse formatting, document text, property resolution, and layout storage
  - use inline containers for interpolations, callbacks, wrapping, and matcher state
  - optimize single-line insertion by mutating document lines without splitting
  - iterate PATH entries directly in Sys::which and reuse candidate buffers
  - avoid temporary clocks and string copies in frequent ecode plugin updates
  - retain documents during autocomplete cache jobs and prevent duplicate scheduling
  - fix PatternMatcher copy assignment and MSVC move-only container compilation
  - expand coverage for matching, text insertion, reusable strings, and Sys::which
This commit is contained in:
Martín Lucas Golini
2026-08-09 21:38:17 -03:00
parent 5f419ca58d
commit 23cf9084ce
35 changed files with 618 additions and 189 deletions
+11 -8
View File
@@ -1568,14 +1568,15 @@ void App::updateDocInfo( TextDocument& doc ) {
mDocInfo && mSplitter->curEditorExistsAndFocused() ) {
mDocInfo->setVisible( true );
updateDocInfoLocation();
String infoStr( String::format(
"%s: %lld / %zu %s: %lld %s %s%s %s", i18n( "line_abbr", "line" ).toUtf8(),
doc.getSelection().start().line() + 1, doc.linesCount(),
i18n( "col_abbr", "col" ).toUtf8(), mSplitter->getCurEditor()->getCurrentColumnCount(),
doc.getSyntaxDefinition().getLanguageName(),
TextFormat::encodingToString( doc.getEncoding() ), doc.isBOM() ? " (with BOM)"sv : ""sv,
TextFormat::lineEndingToString( doc.getLineEnding() ) ) );
mDocInfo->debounce( [this, infoStr] { mDocInfo->setText( infoStr ); }, Time::Zero,
String::formatTo( mDocInfoUtf8Buffer, "%s: %lld / %zu %s: %lld %s %s%s %s",
mDocInfoLineAbbr, doc.getSelection().start().line() + 1, doc.linesCount(),
mDocInfoColAbbr, mSplitter->getCurEditor()->getCurrentColumnCount(),
doc.getSyntaxDefinition().getLanguageName(),
TextFormat::encodingToString( doc.getEncoding() ),
doc.isBOM() ? " (with BOM)"sv : ""sv,
TextFormat::lineEndingToString( doc.getLineEnding() ) );
mDocInfoText.assignUtf8( mDocInfoUtf8Buffer );
mDocInfo->debounce( [this] { mDocInfo->setText( mDocInfoText ); }, Time::Zero,
String::hash( "ecode::doc_info::update" ) );
}
}
@@ -4872,6 +4873,8 @@ void App::init( InitParameters& params ) {
mUISceneNode->bind( "code_container", mBaseLayout );
mUISceneNode->bind( "image_container", mImageLayout );
mUISceneNode->bind( "doc_info", mDocInfo );
mDocInfoLineAbbr = i18n( "line_abbr", "line" ).toUtf8();
mDocInfoColAbbr = i18n( "col_abbr", "col" ).toUtf8();
mUISceneNode->bind( "panel", mSidePanel );
mUISceneNode->bind( "project_splitter", mProjectSplitter );
mUISceneNode->bind( "main_menubar", mMenuBar );
+4
View File
@@ -668,6 +668,10 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
UILayout* mBaseLayout{ nullptr };
UILayout* mImageLayout{ nullptr };
UITextView* mDocInfo{ nullptr };
std::string mDocInfoLineAbbr;
std::string mDocInfoColAbbr;
std::string mDocInfoUtf8Buffer;
String mDocInfoText;
std::vector<std::string> mRecentFiles;
std::stack<std::string> mRecentClosedFiles;
std::unordered_map<std::string, TextRanges> mClosedDocumentState;
@@ -1041,22 +1041,19 @@ bool AutoCompletePlugin::onTextInput( UICodeEditor* editor, const TextInputEvent
return false;
}
void AutoCompletePlugin::updateDocCache( TextDocument* doc ) {
ScopedOp op(
[this, doc] {
Lock lu( mDocsUpdatingMutex );
mDocsUpdating[doc] = true;
},
[this, doc] {
Lock lu( mDocsUpdatingMutex );
mDocsUpdating[doc] = false;
} );
void AutoCompletePlugin::updateDocCache( std::shared_ptr<TextDocument> doc ) {
TextDocument* docPtr = doc.get();
ScopedOp op( [] {},
[this, docPtr] {
Lock lu( mDocsUpdatingMutex );
mDocsUpdating[docPtr] = false;
} );
Clock clock;
std::unordered_map<TextDocument*, DocCache>::iterator docCache;
{
Lock l( mDocMutex );
docCache = mDocCache.find( doc );
docCache = mDocCache.find( docPtr );
if ( docCache == mDocCache.end() || mShuttingDown )
return;
}
@@ -1066,7 +1063,7 @@ void AutoCompletePlugin::updateDocCache( TextDocument* doc ) {
{
Lock l( mDocMutex );
docCache = mDocCache.find( doc );
docCache = mDocCache.find( docPtr );
if ( docCache == mDocCache.end() || mShuttingDown )
return;
auto& cache = docCache->second;
@@ -1966,14 +1963,20 @@ void AutoCompletePlugin::update( UICodeEditor* editor ) {
Lock l( mDocMutex );
for ( auto& doc : mDocs ) {
if ( !doc->isLoading() && mDocCache[doc].changeId != doc->getCurrentChangeId() ) {
auto docRef = getPluginContext()->getSplitter()->getTextDocumentRef( doc );
if ( !docRef )
continue;
{
Lock lu( mDocsUpdatingMutex );
auto du = mDocsUpdating.find( doc );
// Dont update the document cache if it's still updating the document
if ( du != mDocsUpdating.end() && du->second == true )
auto& updating = mDocsUpdating[doc];
// Don't queue another cache update while one is queued or running.
if ( updating )
continue;
updating = true;
}
mThreadPool->run( [this, doc] { updateDocCache( doc ); } );
mThreadPool->run( [this, doc = std::move( docRef )]() mutable {
updateDocCache( std::move( doc ) );
} );
}
}
}
@@ -2484,13 +2487,13 @@ void AutoCompletePlugin::resetSignatureHelp() {
mSignatureHelpEditor = nullptr;
}
AutoCompletePlugin::SymbolsList AutoCompletePlugin::getDocumentSymbols( TextDocument* doc ) {
AutoCompletePlugin::SymbolsList
AutoCompletePlugin::getDocumentSymbols( const std::shared_ptr<TextDocument>& docRef ) {
static constexpr auto MAX_LINE_COUNT = EE_1KB * 10;
AutoCompletePlugin::SymbolsList symbols;
std::shared_ptr<TextDocument> docRef =
getPluginContext()->getSplitter()->getTextDocumentRef( doc ); // acquire a doc
if ( docRef == nullptr )
if ( !docRef )
return symbols;
TextDocument* doc = docRef.get();
LuaPattern pattern( mSymbolPattern );
if ( doc->linesCount() == 0 || doc->isHuge() || mShuttingDown )
return symbols;
@@ -266,9 +266,9 @@ class AutoCompletePlugin : public Plugin {
void updateSuggestions( const std::string& symbol, UICodeEditor* editor );
SymbolsList getDocumentSymbols( TextDocument* );
SymbolsList getDocumentSymbols( const std::shared_ptr<TextDocument>& doc );
void updateDocCache( TextDocument* doc );
void updateDocCache( std::shared_ptr<TextDocument> doc );
std::string getPartialSymbol( TextDocument* doc );
@@ -746,9 +746,9 @@ void LinterPlugin::onUnregister( UICodeEditor* editor ) {
void LinterPlugin::update( UICodeEditor* editor ) {
std::shared_ptr<TextDocument> doc = editor->getDocumentRef();
auto it = mDirtyDoc.find( doc.get() );
if ( it != mDirtyDoc.end() && it->second->getElapsedTime() >= mDelayTime ) {
if ( it != mDirtyDoc.end() && it->second.getElapsedTime() >= mDelayTime ) {
mDirtyDoc.erase( doc.get() );
mThreadPool->run( [this, doc] { lintDoc( doc ); } );
mThreadPool->run( [this, doc = std::move( doc )] { lintDoc( doc ); } );
}
}
@@ -1469,10 +1469,12 @@ Linter LinterPlugin::supportsLinter( std::shared_ptr<TextDocument> doc ) {
}
void LinterPlugin::setDocDirty( TextDocument* doc ) {
mDirtyDoc[doc] = std::make_unique<Clock>();
auto [it, inserted] = mDirtyDoc.try_emplace( doc );
if ( !inserted )
it->second.restart();
}
void LinterPlugin::setDocDirty( UICodeEditor* editor ) {
mDirtyDoc[editor->getDocumentRef().get()] = std::make_unique<Clock>();
setDocDirty( editor->getDocumentRef().get() );
}
void LinterPlugin::invalidateEditors( TextDocument* doc ) {
@@ -131,7 +131,7 @@ class LinterPlugin : public Plugin {
std::unordered_map<UICodeEditor*, std::vector<Uint32>> mEditors;
std::unordered_set<TextDocument*> mDocs;
std::unordered_map<UICodeEditor*, TextDocument*> mEditorDocs;
std::unordered_map<TextDocument*, std::unique_ptr<Clock>> mDirtyDoc;
std::unordered_map<TextDocument*, Clock> mDirtyDoc;
std::unordered_map<TextDocument*, std::map<Int64, std::vector<LinterMatch>>> mMatches;
Time mDelayTime{ Seconds( 0.5f ) };
Mutex mDocMutex;
@@ -230,19 +230,21 @@ void SpellCheckerPlugin::onRegisterListeners( UICodeEditor* editor,
}
void SpellCheckerPlugin::setDocDirty( TextDocument* doc ) {
mDirtyDoc[doc] = std::make_unique<Clock>();
auto [it, inserted] = mDirtyDoc.try_emplace( doc );
if ( !inserted )
it->second.restart();
}
void SpellCheckerPlugin::setDocDirty( UICodeEditor* editor ) {
mDirtyDoc[editor->getDocumentRef().get()] = std::make_unique<Clock>();
setDocDirty( editor->getDocumentRef().get() );
}
void SpellCheckerPlugin::update( UICodeEditor* editor ) {
std::shared_ptr<TextDocument> doc = editor->getDocumentRef();
auto it = mDirtyDoc.find( doc.get() );
if ( it != mDirtyDoc.end() && it->second->getElapsedTime() >= mDelayTime ) {
if ( it != mDirtyDoc.end() && it->second.getElapsedTime() >= mDelayTime ) {
mDirtyDoc.erase( doc.get() );
mThreadPool->run( [this, doc] { spellCheckDoc( doc ); } );
mThreadPool->run( [this, doc = std::move( doc )] { spellCheckDoc( doc ); } );
}
}
@@ -254,11 +256,11 @@ void SpellCheckerPlugin::spellCheckDoc( std::shared_ptr<TextDocument> doc ) {
return;
ScopedOp op(
[this, doc]() {
[this] {
std::lock_guard l( mWorkMutex );
mWorkersCount++;
},
[this]() {
[this] {
{
std::lock_guard l( mWorkMutex );
mWorkersCount--;
@@ -57,7 +57,7 @@ class SpellCheckerPlugin : public PluginBase {
protected:
Time mDelayTime{ Seconds( 0.5f ) };
std::unordered_map<TextDocument*, std::unique_ptr<Clock>> mDirtyDoc;
std::unordered_map<TextDocument*, Clock> mDirtyDoc;
std::unordered_map<TextDocument*, std::map<Int64, std::vector<SpellCheckerMatch>>> mMatches;
std::set<std::string> mLanguagesDisabled;
std::set<std::string> mLSPLanguagesDisabled;
@@ -253,7 +253,7 @@ void XMLToolsPlugin::XMLToolsClient::updateMatch( const TextRange& sel ) {
const auto& line = mDoc->line( mDoc->getSelection().start().line() ).getText();
if ( mDoc->getSelection().start().column() >= (Int64)line.size() )
return clearMatch();
auto def = mDoc->getHighlighter()->getSyntaxDefinitionFromTextPosition( sel.start() );
const auto& def = mDoc->getHighlighter()->getSyntaxDefinitionFromTextPosition( sel.start() );
if ( !def.getAutoCloseXMLTags() ) // getAutoCloseXMLTags means that it supports XML element tags
return clearMatch();
TextRange range = mDoc->getWordRangeInPosition( sel.start(), false );