diff options
author | Neil <nyamatongwe@gmail.com> | 2023-03-02 22:02:19 +1100 |
---|---|---|
committer | Neil <nyamatongwe@gmail.com> | 2023-03-02 22:02:19 +1100 |
commit | 43ba96d61994db084e61b31df278d2d87c1f313c (patch) | |
tree | 9ec2084b3a8594434889fb622f2fe7c5446dc0a9 | |
parent | c61df8742a4865ac9c67f8ed017248b82fe5574e (diff) | |
download | scintilla-mirror-43ba96d61994db084e61b31df278d2d87c1f313c.tar.gz |
Add multithreaded wrap to significantly improve performance of wrapping large
files.
-rw-r--r-- | doc/ScintillaDoc.html | 2 | ||||
-rw-r--r-- | doc/ScintillaHistory.html | 3 | ||||
-rw-r--r-- | src/Document.cxx | 4 | ||||
-rw-r--r-- | src/Document.h | 5 | ||||
-rw-r--r-- | src/EditView.cxx | 9 | ||||
-rw-r--r-- | src/EditView.h | 2 | ||||
-rw-r--r-- | src/Editor.cxx | 138 | ||||
-rw-r--r-- | src/Editor.h | 1 | ||||
-rw-r--r-- | src/PositionCache.cxx | 23 | ||||
-rw-r--r-- | src/PositionCache.h | 9 |
10 files changed, 180 insertions, 16 deletions
diff --git a/doc/ScintillaDoc.html b/doc/ScintillaDoc.html index 30411853e..0b8594f79 100644 --- a/doc/ScintillaDoc.html +++ b/doc/ScintillaDoc.html @@ -7982,7 +7982,7 @@ sptr_t CallScintilla(unsigned int iMessage, uptr_t wParam, sptr_t lParam){ <p><b id="SCI_SETLAYOUTTHREADS">SCI_SETLAYOUTTHREADS(int threads)</b><br /> <b id="SCI_GETLAYOUTTHREADS">SCI_GETLAYOUTTHREADS → int</b><br /> - The time taken to measure text runs on wide lines can be improved by performing the task + The time taken to measure text runs on wide lines or when wrapping can be improved by performing the task concurrently on multiple threads when <a class="seealso" href="#SCI_SUPPORTSFEATURE">SCI_SUPPORTSFEATURE(SC_SUPPORTS_THREAD_SAFE_MEASURE_WIDTHS)</a> is available. diff --git a/doc/ScintillaHistory.html b/doc/ScintillaHistory.html index 7e544789c..9b2d6ccfb 100644 --- a/doc/ScintillaHistory.html +++ b/doc/ScintillaHistory.html @@ -587,6 +587,9 @@ Released 8 February 2023. </li> <li> + Add multithreaded wrap to significantly improve performance of wrapping large files. + </li> + <li> More typesafe bindings of *Full APIs in ScintillaCall. <a href="https://sourceforge.net/p/scintilla/feature-requests/1477/">Feature #1477</a>. </li> diff --git a/src/Document.cxx b/src/Document.cxx index 9d1bf1d3b..4450954b4 100644 --- a/src/Document.cxx +++ b/src/Document.cxx @@ -448,6 +448,10 @@ Sci_Position SCI_METHOD Document::LineStart(Sci_Position line) const { return cb.LineStart(line); } +Range Document::LineRange(Sci::Line line) const noexcept { + return {cb.LineStart(line), cb.LineStart(line + 1)}; +} + bool Document::IsLineStartPosition(Sci::Position position) const { return LineStart(LineFromPosition(position)) == position; } diff --git a/src/Document.h b/src/Document.h index 9f4811073..72ed532b3 100644 --- a/src/Document.h +++ b/src/Document.h @@ -50,6 +50,10 @@ public: return start == end; } + [[nodiscard]] Sci::Position Length() const noexcept { + return (start <= end) ? (end - start) : (start - end); + } + Sci::Position First() const noexcept { return (start <= end) ? start : end; } @@ -450,6 +454,7 @@ public: int MarkerNumberFromLine(Sci::Line line, int which) const noexcept; int MarkerHandleFromLine(Sci::Line line, int which) const noexcept; Sci_Position SCI_METHOD LineStart(Sci_Position line) const override; + [[nodiscard]] Range LineRange(Sci::Line line) const noexcept; bool IsLineStartPosition(Sci::Position position) const; Sci_Position SCI_METHOD LineEnd(Sci_Position line) const override; Sci::Position LineEndPosition(Sci::Position position) const; diff --git a/src/EditView.cxx b/src/EditView.cxx index 259ebf30d..09a20eda3 100644 --- a/src/EditView.cxx +++ b/src/EditView.cxx @@ -395,7 +395,7 @@ void LayoutSegments(IPositionCache *pCache, * Copy the given @a line and its styles from the document into local arrays. * Also determine the x position at which each character starts. */ -void EditView::LayoutLine(const EditModel &model, Surface *surface, const ViewStyle &vstyle, LineLayout *ll, int width) { +void EditView::LayoutLine(const EditModel &model, Surface *surface, const ViewStyle &vstyle, LineLayout *ll, int width, bool callerMultiThreaded) { if (!ll) return; const Sci::Line line = ll->LineNumber(); @@ -491,7 +491,7 @@ void EditView::LayoutLine(const EditModel &model, Surface *surface, const ViewSt const size_t threadsForLength = std::max(1, numCharsInLine / bytesPerLayoutThread); size_t threads = std::min<size_t>({ segments.size(), threadsForLength, maxLayoutThreads }); - if (!surface->SupportsFeature(Supports::ThreadSafeMeasureWidths)) { + if (!surface->SupportsFeature(Supports::ThreadSafeMeasureWidths) || callerMultiThreaded) { threads = 1; } @@ -499,6 +499,7 @@ void EditView::LayoutLine(const EditModel &model, Surface *surface, const ViewSt const bool textUnicode = CpUtf8 == model.pdoc->dbcsCodePage; const bool multiThreaded = threads > 1; + const bool multiThreadedContext = multiThreaded || callerMultiThreaded; IPositionCache *pCache = posCache.get(); // If only 1 thread needed then use the main thread, else spin up multiple @@ -508,8 +509,8 @@ void EditView::LayoutLine(const EditModel &model, Surface *surface, const ViewSt for (size_t th = 0; th < threads; th++) { // Find relative positions of everything except for tabs std::future<void> fut = std::async(policy, - [pCache, surface, &vstyle, &ll, &segments, &nextIndex, textUnicode, multiThreaded]() { - LayoutSegments(pCache, surface, vstyle, ll, segments, nextIndex, textUnicode, multiThreaded); + [pCache, surface, &vstyle, &ll, &segments, &nextIndex, textUnicode, multiThreadedContext]() { + LayoutSegments(pCache, surface, vstyle, ll, segments, nextIndex, textUnicode, multiThreadedContext); }); futures.push_back(std::move(fut)); } diff --git a/src/EditView.h b/src/EditView.h index affb4663e..1e28d25c5 100644 --- a/src/EditView.h +++ b/src/EditView.h @@ -119,7 +119,7 @@ public: std::shared_ptr<LineLayout> RetrieveLineLayout(Sci::Line lineNumber, const EditModel &model); void LayoutLine(const EditModel &model, Surface *surface, const ViewStyle &vstyle, - LineLayout *ll, int width); + LineLayout *ll, int width, bool callerMultiThreaded=false); static void UpdateBidiData(const EditModel &model, const ViewStyle &vstyle, LineLayout *ll); diff --git a/src/Editor.cxx b/src/Editor.cxx index d64a09deb..30a9c9786 100644 --- a/src/Editor.cxx +++ b/src/Editor.cxx @@ -25,6 +25,9 @@ #include <iterator> #include <memory> #include <chrono> +#include <atomic> +#include <thread> +#include <future> #include "ScintillaTypes.h" #include "ScintillaMessages.h" @@ -1473,6 +1476,130 @@ bool Editor::WrapOneLine(Surface *surface, Sci::Line lineToWrap) { return pcs->SetHeight(lineToWrap, linesWrapped); } +namespace { + +// Lines less than lengthToMultiThread are laid out in blocks in parallel. +// Longer lines are multi-threaded inside LayoutLine. +// This allows faster processing when lines differ greatly in length and thus time to lay out. +constexpr Sci::Position lengthToMultiThread = 4000; + +} + +bool Editor::WrapBlock(Surface *surface, Sci::Line lineToWrap, Sci::Line lineToWrapEnd) { + + const size_t linesBeingWrapped = static_cast<size_t>(lineToWrapEnd - lineToWrap); + + std::vector<int> linesAfterWrap(linesBeingWrapped); + + size_t threads = std::min<size_t>({ linesBeingWrapped, view.maxLayoutThreads }); + if (!surface->SupportsFeature(Supports::ThreadSafeMeasureWidths)) { + threads = 1; + } + + const bool multiThreaded = threads > 1; + + ElapsedPeriod epWrapping; + + // Wrap all the short lines in multiple threads + + // If only 1 thread needed then use the main thread, else spin up multiple + const std::launch policy = multiThreaded ? std::launch::async : std::launch::deferred; + + std::atomic<size_t> nextIndex = 0; + + // Lines that are less likely to be re-examined should not be read from or written to the cache. + const SignificantLines significantLines { + pdoc->SciLineFromPosition(sel.MainCaret()), + topLine, + LinesOnScreen() + 1, + view.llc.GetLevel(), + }; + + // Protect the line layout cache from being accessed from multiple threads simultaneously + std::mutex mutexRetrieve; + + std::vector<std::future<void>> futures; + for (size_t th = 0; th < threads; th++) { + std::future<void> fut = std::async(policy, + [=, &surface, &nextIndex, &linesAfterWrap, &mutexRetrieve]() { + // llTemporary is reused for non-significant lines, avoiding allocation costs. + std::shared_ptr<LineLayout> llTemporary = std::make_shared<LineLayout>(-1, 200); + while (true) { + const size_t i = nextIndex.fetch_add(1, std::memory_order_acq_rel); + if (i >= linesBeingWrapped) { + break; + } + const Sci::Line lineNumber = lineToWrap + i; + const Range rangeLine = pdoc->LineRange(lineNumber); + const Sci::Position lengthLine = rangeLine.Length(); + if (lengthLine < lengthToMultiThread) { + std::shared_ptr<LineLayout> ll; + if (significantLines.LineMayCache(lineNumber)) { + std::lock_guard<std::mutex> guard(mutexRetrieve); + ll = view.RetrieveLineLayout(lineNumber, *this); + } else { + ll = llTemporary; + ll->ReSet(lineNumber, lengthLine); + } + view.LayoutLine(*this, surface, vs, ll.get(), wrapWidth, multiThreaded); + linesAfterWrap[i] = ll->lines; + } + } + }); + futures.push_back(std::move(fut)); + } + for (const std::future<void> &f : futures) { + f.wait(); + } + // End of multiple threads + + // Multiply duration by number of threads to produce (near) equivalence to duration if single threaded + const double durationShortLines = epWrapping.Duration(true); + const double durationShortLinesThreads = durationShortLines * threads; + + // Wrap all the long lines in the main thread. + // LayoutLine may then multi-thread over segments in each line. + + std::shared_ptr<LineLayout> llLarge = std::make_shared<LineLayout>(-1, 200); + for (size_t indexLarge = 0; indexLarge < linesBeingWrapped; indexLarge++) { + const Sci::Line lineNumber = lineToWrap + indexLarge; + const Range rangeLine = pdoc->LineRange(lineNumber); + const Sci::Position lengthLine = rangeLine.Length(); + if (lengthLine >= lengthToMultiThread) { + std::shared_ptr<LineLayout> ll; + if (significantLines.LineMayCache(lineNumber)) { + ll = view.RetrieveLineLayout(lineNumber, *this); + } else { + ll = llLarge; + ll->ReSet(lineNumber, lengthLine); + } + view.LayoutLine(*this, surface, vs, ll.get(), wrapWidth); + linesAfterWrap[indexLarge] = ll->lines; + } + } + + const double durationLongLines = epWrapping.Duration(); + const size_t bytesBeingWrapped = pdoc->LineStart(lineToWrap + linesBeingWrapped) - pdoc->LineStart(lineToWrap); + + size_t wrapsDone = 0; + + for (size_t i = 0; i < linesBeingWrapped; i++) { + const Sci::Line lineNumber = lineToWrap + i; + int linesWrapped = linesAfterWrap[i]; + if (vs.annotationVisible != AnnotationVisible::Hidden) { + linesWrapped += pdoc->AnnotationLines(lineNumber); + } + if (pcs->SetHeight(lineNumber, linesWrapped)) { + wrapsDone++; + } + wrapPending.Wrapped(lineNumber); + } + + durationWrapOneByte.AddSample(bytesBeingWrapped, durationShortLinesThreads + durationLongLines); + + return wrapsDone > 0; +} + // Perform wrapping for a subset of the lines needing wrapping. // wsAll: wrap all lines which need wrapping in this single call // wsVisible: wrap currently visible lines @@ -1554,16 +1681,7 @@ bool Editor::WrapLines(WrapScope ws) { if (surface) { //Platform::DebugPrintf("Wraplines: scope=%0d need=%0d..%0d perform=%0d..%0d\n", ws, wrapPending.start, wrapPending.end, lineToWrap, lineToWrapEnd); - const size_t bytesBeingWrapped = pdoc->LineStart(lineToWrapEnd) - pdoc->LineStart(lineToWrap); - ElapsedPeriod epWrapping; - while (lineToWrap < lineToWrapEnd) { - if (WrapOneLine(surface, lineToWrap)) { - wrapOccurred = true; - } - wrapPending.Wrapped(lineToWrap); - lineToWrap++; - } - durationWrapOneByte.AddSample(bytesBeingWrapped, epWrapping.Duration()); + wrapOccurred = WrapBlock(surface, lineToWrap, lineToWrapEnd); goodTopLine = pcs->DisplayFromDoc(lineDocTop) + std::min( subLineTop, static_cast<Sci::Line>(pcs->GetHeight(lineDocTop)-1)); diff --git a/src/Editor.h b/src/Editor.h index 61e0dd5b0..f9d938685 100644 --- a/src/Editor.h +++ b/src/Editor.h @@ -400,6 +400,7 @@ protected: // ScintillaBase subclass needs access to much of Editor bool Wrapping() const noexcept; void NeedWrapping(Sci::Line docLineStart=0, Sci::Line docLineEnd=WrapPending::lineLarge); bool WrapOneLine(Surface *surface, Sci::Line lineToWrap); + bool WrapBlock(Surface *surface, Sci::Line lineToWrap, Sci::Line lineToWrapEnd); enum class WrapScope {wsAll, wsVisible, wsIdle}; bool WrapLines(WrapScope ws); void LinesJoin(); diff --git a/src/PositionCache.cxx b/src/PositionCache.cxx index 30d76b1d3..9f4e4edc7 100644 --- a/src/PositionCache.cxx +++ b/src/PositionCache.cxx @@ -102,6 +102,13 @@ void LineLayout::Resize(int maxLineLength_) { } } +void LineLayout::ReSet(Sci::Line lineNumber_, Sci::Position maxLineLength_) { + lineNumber = lineNumber_; + Resize(static_cast<int>(maxLineLength_)); + lines = 0; + Invalidate(ValidLevel::invalid); +} + void LineLayout::EnsureBidiData() { if (!bidiData) { bidiData = std::make_unique<BidiData>(); @@ -114,6 +121,7 @@ void LineLayout::Free() noexcept { styles.reset(); positions.reset(); lineStarts.reset(); + lenLineStarts = 0; bidiData.reset(); } @@ -446,6 +454,21 @@ XYPOSITION ScreenLine::TabPositionAfter(XYPOSITION xPosition) const { return (std::floor((xPosition + TabWidthMinimumPixels()) / TabWidth()) + 1) * TabWidth(); } +bool SignificantLines::LineMayCache(Sci::Line line) const noexcept { + switch (level) { + case LineCache::None: + return false; + case LineCache::Caret: + return line == lineCaret; + case LineCache::Page: + return (abs(line - lineCaret) < linesOnScreen) || + ((line >= lineTop) && (line <= (lineTop + linesOnScreen))); + case LineCache::Document: + default: + return true; + } +} + LineLayoutCache::LineLayoutCache() : level(LineCache::None), allInvalidated(false), styleClock(-1) { diff --git a/src/PositionCache.h b/src/PositionCache.h index 0add8319a..e610ac879 100644 --- a/src/PositionCache.h +++ b/src/PositionCache.h @@ -83,6 +83,7 @@ public: void operator=(LineLayout &&) = delete; virtual ~LineLayout(); void Resize(int maxLineLength_); + void ReSet(Sci::Line lineNumber_, Sci::Position maxLineLength_); void EnsureBidiData(); void Free() noexcept; void ClearPositions(); @@ -140,6 +141,14 @@ struct ScreenLine : public IScreenLine { XYPOSITION TabPositionAfter(XYPOSITION xPosition) const override; }; +struct SignificantLines { + Sci::Line lineCaret; + Sci::Line lineTop; + Sci::Line linesOnScreen; + Scintilla::LineCache level; + bool LineMayCache(Sci::Line line) const noexcept; +}; + /** */ class LineLayoutCache { |