From f50b197c5869d50e8fb88c7bbf3763a47b8eddd0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Feb 2017 16:21:50 -0800 Subject: [PATCH 1/2] Avoid hangs when opening minified files * Force soft wraps for lines that exceed 500 chars * Don't pass more than 500 chars to first-mate --- ...xt-editor-large-file-construction.bench.js | 2 +- benchmarks/text-editor-long-lines.js | 97 +++++++++++++++++++ src/text-editor.coffee | 3 +- src/tokenized-buffer.coffee | 4 + 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 benchmarks/text-editor-long-lines.js diff --git a/benchmarks/text-editor-large-file-construction.bench.js b/benchmarks/text-editor-large-file-construction.bench.js index 694729724..64c6f4d94 100644 --- a/benchmarks/text-editor-large-file-construction.bench.js +++ b/benchmarks/text-editor-large-file-construction.bench.js @@ -26,7 +26,7 @@ export default async function ({test}) { console.log(text.length / 1024) let t0 = window.performance.now() - const buffer = new TextBuffer(text) + const buffer = new TextBuffer({text}) const editor = new TextEditor({buffer, largeFileMode: true}) atom.workspace.getActivePane().activateItem(editor) let t1 = window.performance.now() diff --git a/benchmarks/text-editor-long-lines.js b/benchmarks/text-editor-long-lines.js new file mode 100644 index 000000000..a99220d4e --- /dev/null +++ b/benchmarks/text-editor-long-lines.js @@ -0,0 +1,97 @@ +/** @babel */ + +import path from 'path' +import fs from 'fs' +import {TextEditor, TextBuffer} from 'atom' + +const SIZES_IN_KB = [ + 512, + 1024, + 2048 +] +const REPEATED_TEXT = fs.readFileSync(path.join(__dirname, '..', 'spec', 'fixtures', 'sample.js'), 'utf8').replace(/\n/g, '') +const TEXT = REPEATED_TEXT.repeat(Math.ceil(SIZES_IN_KB[SIZES_IN_KB.length - 1] * 1024 / REPEATED_TEXT.length)) + +export default async function ({test}) { + const data = [] + + const workspaceElement = atom.views.getView(atom.workspace) + document.body.appendChild(workspaceElement) + + atom.packages.loadPackages() + await atom.packages.activate() + + console.log(atom.getLoadSettings().resourcePath); + + for (let pane of atom.workspace.getPanes()) { + pane.destroy() + } + + for (const sizeInKB of SIZES_IN_KB) { + const text = TEXT.slice(0, sizeInKB * 1024) + console.log(text.length / 1024) + + let t0 = window.performance.now() + const buffer = new TextBuffer({text}) + const editor = new TextEditor({buffer, largeFileMode: true}) + editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) + atom.workspace.getActivePane().activateItem(editor) + let t1 = window.performance.now() + + data.push({ + name: 'Opening a large single-line file', + x: sizeInKB, + duration: t1 - t0 + }) + + const tickDurations = [] + for (let i = 0; i < 20; i++) { + await timeout(50) + t0 = window.performance.now() + await timeout(0) + t1 = window.performance.now() + tickDurations[i] = t1 - t0 + } + + data.push({ + name: 'Max time event loop was blocked after opening a large single-line file', + x: sizeInKB, + duration: Math.max(...tickDurations) + }) + + t0 = window.performance.now() + editor.setCursorScreenPosition(editor.element.screenPositionForPixelPosition({ + top: 100, + left: 30 + })) + t1 = window.performance.now() + + data.push({ + name: 'Clicking the editor after opening a large single-line file', + x: sizeInKB, + duration: t1 - t0 + }) + + t0 = window.performance.now() + editor.element.setScrollTop(editor.element.getScrollTop() + 100) + t1 = window.performance.now() + + data.push({ + name: 'Scrolling down after opening a large single-line file', + x: sizeInKB, + duration: t1 - t0 + }) + + editor.destroy() + buffer.destroy() + await timeout(10000) + } + + workspaceElement.remove() + + return data +} + +function timeout (duration) { + return new Promise((resolve) => setTimeout(resolve, duration)) +} diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 10a6c9783..00d6297ee 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -16,6 +16,7 @@ TextEditorElement = require './text-editor-element' {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' ZERO_WIDTH_NBSP = '\ufeff' +MAX_SCREEN_LINE_LENGTH = 500 # Essential: This class represents all essential editing state for a single # {TextBuffer}, including cursor and selection positions, folds, and soft wraps. @@ -2964,7 +2965,7 @@ class TextEditor extends Model else @getEditorWidthInChars() else - Infinity + MAX_SCREEN_LINE_LENGTH ### Section: Indentation diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 234f82be9..77221f52e 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -8,6 +8,8 @@ ScopeDescriptor = require './scope-descriptor' TokenizedBufferIterator = require './tokenized-buffer-iterator' NullGrammar = require './null-grammar' +MAX_LINE_LENGTH_TO_TOKENIZE = 500 + module.exports = class TokenizedBuffer extends Model grammar: null @@ -251,6 +253,8 @@ class TokenizedBuffer extends Model buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) -> lineEnding = @buffer.lineEndingForRow(row) + if text.length > MAX_LINE_LENGTH_TO_TOKENIZE + text = text.slice(0, MAX_LINE_LENGTH_TO_TOKENIZE) {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false) new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator}) From 10ce4d5796c647f3b0b0bae7de036587aadad3a3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Feb 2017 16:48:20 -0800 Subject: [PATCH 2/2] Add .bench extension to long lines benchmark --- ...{text-editor-long-lines.js => text-editor-long-lines.bench.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename benchmarks/{text-editor-long-lines.js => text-editor-long-lines.bench.js} (100%) diff --git a/benchmarks/text-editor-long-lines.js b/benchmarks/text-editor-long-lines.bench.js similarity index 100% rename from benchmarks/text-editor-long-lines.js rename to benchmarks/text-editor-long-lines.bench.js