Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit be5f49fb authored by Gilles Debunne's avatar Gilles Debunne
Browse files

Performance improvements for long text edition.

Limit each parse to batches of a few words, to keep the UI thread
responsive.

Possible optimizations for the future:
- SpellCheck in a thread, but that requires some locking mecanism
- Only spell check what is visible on screen. Will require additional
  spans to tag the pieces of text.

This is a cherry pick of 145656 into ICS-MR1

Patch Set 2: Make the Runnable shared and stop it when detached.

Change-Id: Ibf8e98274bda84b7176aac181ff267fc1f1fa4cb
parent df372289
Loading
Loading
Loading
Loading
+58 −14
Original line number Diff line number Diff line
@@ -43,7 +43,18 @@ import java.util.Locale;
 */
public class SpellChecker implements SpellCheckerSessionListener {

    private final static int MAX_SPELL_BATCH_SIZE = 50;
    // No more than this number of words will be parsed on each iteration to ensure a minimum
    // lock of the UI thread
    public static final int MAX_NUMBER_OF_WORDS = 50;

    // Rough estimate, such that the word iterator interval usually does not need to be shifted
    public static final int AVERAGE_WORD_LENGTH = 7;

    // When parsing, use a character window of that size. Will be shifted if needed
    public static final int WORD_ITERATOR_INTERVAL = AVERAGE_WORD_LENGTH * MAX_NUMBER_OF_WORDS;

    // Pause between each spell check to keep the UI smooth
    private final static int SPELL_PAUSE_DURATION = 400; // milliseconds

    private final TextView mTextView;

@@ -71,6 +82,8 @@ public class SpellChecker implements SpellCheckerSessionListener {

    private TextServicesManager mTextServicesManager;

    private Runnable mSpellRunnable;

    public SpellChecker(TextView textView) {
        mTextView = textView;

@@ -141,6 +154,10 @@ public class SpellChecker implements SpellCheckerSessionListener {
        for (int i = 0; i < length; i++) {
            mSpellParsers[i].finish();
        }

        if (mSpellRunnable != null) {
            mTextView.removeCallbacks(mSpellRunnable);
        }
    }

    private int nextSpellCheckSpanIndex() {
@@ -254,6 +271,7 @@ public class SpellChecker implements SpellCheckerSessionListener {
                System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
                textInfos = textInfosCopy;
            }

            mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
                    false /* TODO Set sequentialWords to true for initial spell check */);
        }
@@ -288,13 +306,29 @@ public class SpellChecker implements SpellCheckerSessionListener {
            }
        }

        scheduleNewSpellCheck();
    }

    private void scheduleNewSpellCheck() {
        if (mSpellRunnable == null) {
            mSpellRunnable = new Runnable() {
                @Override
                public void run() {
                    final int length = mSpellParsers.length;
                    for (int i = 0; i < length; i++) {
                        final SpellParser spellParser = mSpellParsers[i];
                        if (!spellParser.isFinished()) {
                            spellParser.parse();
                            break; // run one spell parser at a time to bound running time
                        }
                    }
                }
            };
        } else {
            mTextView.removeCallbacks(mSpellRunnable);
        }

        mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
    }

    private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
@@ -383,7 +417,9 @@ public class SpellChecker implements SpellCheckerSessionListener {
            // Iterate over the newly added text and schedule new SpellCheckSpans
            final int start = editable.getSpanStart(mRange);
            final int end = editable.getSpanEnd(mRange);
            mWordIterator.setCharSequence(editable, start, end);

            int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
            mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);

            // Move back to the beginning of the current word, if any
            int wordStart = mWordIterator.preceding(start);
@@ -408,11 +444,16 @@ public class SpellChecker implements SpellCheckerSessionListener {
            SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
                    SuggestionSpan.class);

            int nbWordsChecked = 0;
            int wordCount = 0;
            boolean scheduleOtherSpellCheck = false;

            while (wordStart <= end) {
                if (wordEnd >= start && wordEnd > wordStart) {
                    if (wordCount >= MAX_NUMBER_OF_WORDS) {
                        scheduleOtherSpellCheck = true;
                        break;
                    }

                    // A new word has been created across the interval boundaries with this edit.
                    // Previous spans (ended on start / started on end) removed, not valid anymore
                    if (wordStart < start && wordEnd > start) {
@@ -448,17 +489,20 @@ public class SpellChecker implements SpellCheckerSessionListener {
                    }

                    if (createSpellCheckSpan) {
                        if (nbWordsChecked == MAX_SPELL_BATCH_SIZE) {
                            scheduleOtherSpellCheck = true;
                            break;
                        }
                        addSpellCheckSpan(editable, wordStart, wordEnd);
                        nbWordsChecked++;
                    }
                    wordCount++;
                }

                // iterate word by word
                int originalWordEnd = wordEnd;
                wordEnd = mWordIterator.following(wordEnd);
                if ((wordIteratorWindowEnd < end) &&
                        (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
                    wordIteratorWindowEnd = Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
                    mWordIterator.setCharSequence(editable, originalWordEnd, wordIteratorWindowEnd);
                    wordEnd = mWordIterator.following(originalWordEnd);
                }
                if (wordEnd == BreakIterator.DONE) break;
                wordStart = mWordIterator.getBeginning(wordEnd);
                if (wordStart == BreakIterator.DONE) {