Skip to content

Commit

Permalink
Limit the maximum number of highlight markers
Browse files Browse the repository at this point in the history
This places a configurable limit on the number of highlights shown across all editors.
In my tests, scanning a large file (50K+ lines) with a match on each line causes Atom's UI thread
to lock up for several seconds at a time. It doesn't, however, lock up when there are only a few
matches across files of that size, as a lot of time is spent creating and managing the many markers
that result. Likewise, it seems that `editor.scan` takes considerably longer when there are matches
compared to when there aren't on documents of equal size.

By default, the number of markers is limited to 500. On my machine (a 2017-era MacBook Pro), scanning
a 50K line document with matches on each line with this limit applied reliably completes in under 80ms,
which is [well under the threshold for a perceptible delay](https://developers.google.com/web/fundamentals/performance/rail).
I'm definitely open to changing this default, especially if it's smaller :)

This is all implemented by throwing an exception once we've hit our result threshold. It's gross, and
I've tried an early `return` in the `scan` callback, but the scan runs to completion either way and we end up
doing nothing with results that we spent work on. AFAICT, this is the only way we can terminate the scan early
without upstreaming support for this in Atom's native C++ text buffer.

cc @matthewwithanm @captbaritone @sunz7
  • Loading branch information
wbinnssmith committed Nov 28, 2018
1 parent ee83dad commit da2eb3d
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 26 deletions.
4 changes: 4 additions & 0 deletions lib/highlight-selected.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ module.exports =
minimumLength:
type: 'integer'
default: 2
maximumHighlights:
type: 'integer'
default: 500
description: 'For performance purposes, the number of highlights is limited'
timeout:
type: 'integer'
default: 20
Expand Down
77 changes: 51 additions & 26 deletions lib/highlighted-area-view.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -164,37 +164,60 @@ class HighlightedAreaView

highlightSelectionInEditor: (editor, regexSearch, regexFlags, originalEditor) ->
return unless editor?
maximumHighlights = atom.config.get('highlight-selected.maximumHighlights')
return unless this.resultCount < maximumHighlights

markerLayers = @editorToMarkerLayerMap[editor.id]
return unless markerLayers?
markerLayer = markerLayers['visibleMarkerLayer']
markerLayerForHiddenMarkers = markerLayers['selectedMarkerLayer']

editor.scan new RegExp(regexSearch, regexFlags),
(result) =>
newResult = result
if atom.config.get('highlight-selected.onlyHighlightWholeWords')
editor.scanInBufferRange(
new RegExp(escapeRegExp(result.match[1])),
result.range,
(e) -> newResult = e
)

return unless newResult?
@resultCount += 1

if @showHighlightOnSelectedWord(newResult.range, @selections) &&
originalEditor?.id == editor.id
marker = markerLayerForHiddenMarkers.markBufferRange(newResult.range)
@emitter.emit 'did-add-selected-marker', marker
@emitter.emit 'did-add-selected-marker-for-editor',
marker: marker
editor: editor
else
marker = markerLayer.markBufferRange(newResult.range)
@emitter.emit 'did-add-marker', marker
@emitter.emit 'did-add-marker-for-editor',
marker: marker
editor: editor
# HACK: `editor.scan` is a synchronous process which iterates the entire buffer,
# executing a regex against every line and yielding each match. This can be
# costly for very large files with many matches.
#
# While we can and do limit the maximum number of highlight markers,
# `editor.scan` cannot be terminated early, meaning that we are forced to
# pay the cost of iterating every line in the file, running the regex, and
# returning matches, even if we shouldn't be creating any more markers.
#
# Instead, throw an exception. This isn't pretty, but it prevents the
# scan from running to completion unnecessarily.
try
editor.scan new RegExp(regexSearch, regexFlags),
(result) =>
if (this.resultCount >= maximumHighlights)
throw new EarlyTerminationSignal

newResult = result
if atom.config.get('highlight-selected.onlyHighlightWholeWords')
editor.scanInBufferRange(
new RegExp(escapeRegExp(result.match[1])),
result.range,
(e) -> newResult = e
)

return unless newResult?
@resultCount += 1

if @showHighlightOnSelectedWord(newResult.range, @selections) &&
originalEditor?.id == editor.id
marker = markerLayerForHiddenMarkers.markBufferRange(newResult.range)
@emitter.emit 'did-add-selected-marker', marker
@emitter.emit 'did-add-selected-marker-for-editor',
marker: marker
editor: editor
else
marker = markerLayer.markBufferRange(newResult.range)
@emitter.emit 'did-add-marker', marker
@emitter.emit 'did-add-marker-for-editor',
marker: marker
editor: editor
catch error
if error not instanceof EarlyTerminationSignal
# If this is an early termination, just continue on.
throw error

editor.decorateMarkerLayer(markerLayer, {
type: 'highlight',
class: @makeClasses()
Expand Down Expand Up @@ -339,3 +362,5 @@ class HighlightedAreaView

scrollMarkerView = @scrollMarker.scrollMarkerViewForEditor(editor)
scrollMarkerView.destroy()

class EarlyTerminationSignal extends Error

0 comments on commit da2eb3d

Please sign in to comment.