Refactor filter to not be initialized every time

We eagerly compile all the filters up front, then gather the
compiled filters into a DiagnosticFilter lazily, caching the
result to avoid garbage lists.
This commit is contained in:
dhleong 2016-10-22 18:37:13 -04:00
parent b8280c7b19
commit 7978215bca
3 changed files with 110 additions and 41 deletions

View File

@ -24,20 +24,19 @@ standard_library.install_aliases()
from builtins import * # noqa from builtins import * # noqa
from future.utils import iterkeys, iteritems from future.utils import iterkeys, iteritems
from ycm import vimsupport
import re import re
class DiagnosticFilter( object ): class DiagnosticFilter( object ):
def __init__( self, config ): def __init__( self, config_or_filters ):
self._filters = [] if isinstance( config_or_filters, list ):
self._filters = config_or_filters
print( 'NewFilter', config_or_filters)
for filter_type in iterkeys( config ): else:
compiler = FILTER_COMPILERS.get( filter_type ) self._filters = _CompileFilters( config_or_filters )
print( 'CompileFilters', config_or_filters)
if compiler is not None:
for filter_config in _ListOf( config[ filter_type ] ):
compiledFilter = compiler( filter_config )
self._filters.append( compiledFilter )
def IsAllowed( self, diagnostic ): def IsAllowed( self, diagnostic ):
@ -49,19 +48,59 @@ class DiagnosticFilter( object ):
return True return True
@staticmethod def SubsetForTypes( self, filetypes ):
def from_filetype( user_options, filetypes ): """Return a sub-filter limited to the given filetypes"""
spec = {} # NOTE: actually, this class is already filtered
all_filters = dict( user_options.get( 'filter_diagnostics', {} ) ) return self
for typeSpec, filterValue in iteritems( dict( all_filters ) ):
if typeSpec.find(',') != -1:
for filetype in typeSpec.split(','):
all_filters[ filetype ] = filterValue
@staticmethod
def CreateFromOptions( user_options ):
all_filters = dict( user_options.get( 'filter_diagnostics', {} ) )
compiled_by_type = {}
for type_spec, filter_value in iteritems( dict( all_filters ) ):
filetypes = [ type_spec ]
if type_spec.find( ',' ) != -1:
filetypes = type_spec.split( ',' )
for filetype in filetypes:
compiled_by_type[ filetype ] = _CompileFilters( filter_value )
return _MasterDiagnosticFilter( compiled_by_type )
class _MasterDiagnosticFilter( object ):
def __init__( self, all_filters ):
self._all_filters = all_filters
self._cache = {}
def IsAllowed( self, diagnostic ):
# NOTE: in this class's implementation, we ask vimsupport for
# the current filetypes and delegate automatically; it is probably,
# more efficient, however, to call SubsetForTypes() and reuse
# the returned DiagnosticFilter if it will be checked repeatedly.
filetypes = vimsupport.CurrentFiletypes()
return self.SubsetForTypes( filetypes ).IsAllowed( diagnostic )
def SubsetForTypes( self, filetypes ):
# check cache
cache_key = ','.join( filetypes )
cached = self._cache.get( cache_key )
if cached is not None:
return cached
# build a new DiagnosticFilter mergin all filters
# for the provided filetypes
spec = []
for filetype in filetypes: for filetype in filetypes:
type_specific = all_filters.get( filetype, {} ) type_specific = self._all_filters.get( filetype, [] )
spec = _Merge( spec, type_specific ) spec.extend( type_specific )
return DiagnosticFilter( spec )
new_filter = DiagnosticFilter( spec )
self._cache[ cache_key ] = new_filter
return new_filter
def _ListOf( config_entry ): def _ListOf( config_entry ):
@ -74,13 +113,6 @@ def _ListOf( config_entry ):
return [ config_entry ] return [ config_entry ]
def _Merge( into, other ):
for key in iterkeys( other ):
into[ key ] = _ListOf( into.get( key ) ) + _ListOf( other[ key ] )
return into
def _CompileRegex( raw_regex ): def _CompileRegex( raw_regex ):
pattern = re.compile( raw_regex, re.IGNORECASE ) pattern = re.compile( raw_regex, re.IGNORECASE )
@ -104,3 +136,18 @@ def _CompileLevel( level ):
FILTER_COMPILERS = { 'regex' : _CompileRegex, FILTER_COMPILERS = { 'regex' : _CompileRegex,
'level' : _CompileLevel } 'level' : _CompileLevel }
def _CompileFilters( config ):
"""Given a filter config dictionary, return a list of compiled filters"""
filters = []
for filter_type in iterkeys( config ):
compiler = FILTER_COMPILERS.get( filter_type )
if compiler is not None:
for filter_config in _ListOf( config[ filter_type ] ):
compiledFilter = compiler( filter_config )
filters.append( compiledFilter )
return filters

View File

@ -33,6 +33,7 @@ import vim
class DiagnosticInterface( object ): class DiagnosticInterface( object ):
def __init__( self, user_options ): def __init__( self, user_options ):
self._user_options = user_options self._user_options = user_options
self._diag_filter = DiagnosticFilter.CreateFromOptions( user_options )
# Line and column numbers are 1-based # Line and column numbers are 1-based
self._buffer_number_to_line_to_diags = defaultdict( self._buffer_number_to_line_to_diags = defaultdict(
lambda: defaultdict( list ) ) lambda: defaultdict( list ) )
@ -62,15 +63,13 @@ class DiagnosticInterface( object ):
def PopulateLocationList( self, diags ): def PopulateLocationList( self, diags ):
vimsupport.SetLocationList( vimsupport.SetLocationList(
vimsupport.ConvertDiagnosticsToQfList( diags ) ) vimsupport.ConvertDiagnosticsToQfList(
self._ApplyDiagnosticFilter( diags ) ) )
def UpdateWithNewDiagnostics( self, diags ): def UpdateWithNewDiagnostics( self, diags ):
diag_filter = DiagnosticFilter.from_filetype( normalized_diags = [ _NormalizeDiagnostic( x ) for x in
self._user_options, self._ApplyDiagnosticFilter( diags ) ]
vimsupport.CurrentFiletypes() )
normalized_diags = [ _NormalizeDiagnostic( x ) for x in diags
if diag_filter.IsAllowed( x ) ]
self._buffer_number_to_line_to_diags = _ConvertDiagListToDict( self._buffer_number_to_line_to_diags = _ConvertDiagListToDict(
normalized_diags ) normalized_diags )
@ -86,6 +85,13 @@ class DiagnosticInterface( object ):
if self._user_options[ 'always_populate_location_list' ]: if self._user_options[ 'always_populate_location_list' ]:
self.PopulateLocationList( normalized_diags ) self.PopulateLocationList( normalized_diags )
def _ApplyDiagnosticFilter( self, diags ):
filetypes = vimsupport.CurrentFiletypes()
diag_filter = self._diag_filter.SubsetForTypes( filetypes )
return filter( diag_filter.IsAllowed, diags )
def _EchoDiagnosticForLine( self, line_num ): def _EchoDiagnosticForLine( self, line_num ):
buffer_num = vim.current.buffer.number buffer_num = vim.current.buffer.number
diags = self._buffer_number_to_line_to_diags[ buffer_num ][ line_num ] diags = self._buffer_number_to_line_to_diags[ buffer_num ][ line_num ]

View File

@ -24,6 +24,10 @@ standard_library.install_aliases()
from builtins import * # noqa from builtins import * # noqa
from hamcrest import assert_that, equal_to from hamcrest import assert_that, equal_to
from ycm.test_utils import MockVimModule
MockVimModule()
from ycm.diagnostic_filter import DiagnosticFilter from ycm.diagnostic_filter import DiagnosticFilter
@ -46,9 +50,13 @@ def _JavaFilter( config ):
return { 'filter_diagnostics' : { 'java': config } } return { 'filter_diagnostics' : { 'java': config } }
def _CreateFilterForTypes( opts, types ):
return DiagnosticFilter.CreateFromOptions( opts ).SubsetForTypes( types )
def RegexFilter_test(): def RegexFilter_test():
opts = _JavaFilter( { 'regex' : 'taco' } ) opts = _JavaFilter( { 'regex' : 'taco' } )
f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) f = _CreateFilterForTypes( opts, [ 'java' ] )
_assert_rejects( f, 'This is a Taco' ) _assert_rejects( f, 'This is a Taco' )
_assert_accepts( f, 'This is a Burrito' ) _assert_accepts( f, 'This is a Burrito' )
@ -56,7 +64,7 @@ def RegexFilter_test():
def RegexSingleList_test(): def RegexSingleList_test():
opts = _JavaFilter( { 'regex' : [ 'taco' ] } ) opts = _JavaFilter( { 'regex' : [ 'taco' ] } )
f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) f = _CreateFilterForTypes( opts, [ 'java' ] )
_assert_rejects( f, 'This is a Taco' ) _assert_rejects( f, 'This is a Taco' )
_assert_accepts( f, 'This is a Burrito' ) _assert_accepts( f, 'This is a Burrito' )
@ -64,15 +72,23 @@ def RegexSingleList_test():
def RegexMultiList_test(): def RegexMultiList_test():
opts = _JavaFilter( { 'regex' : [ 'taco', 'burrito' ] } ) opts = _JavaFilter( { 'regex' : [ 'taco', 'burrito' ] } )
f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) f = _CreateFilterForTypes( opts, [ 'java' ] )
_assert_rejects( f, 'This is a Taco' ) _assert_rejects( f, 'This is a Taco' )
_assert_rejects( f, 'This is a Burrito' ) _assert_rejects( f, 'This is a Burrito' )
def RegexNotFiltered_test():
opts = _JavaFilter( { 'regex' : 'taco' } )
f = _CreateFilterForTypes( opts, [ 'cs' ] )
_assert_accepts( f, 'This is a Taco' )
_assert_accepts( f, 'This is a Burrito' )
def LevelWarnings_test(): def LevelWarnings_test():
opts = _JavaFilter( { 'level' : 'warning' } ) opts = _JavaFilter( { 'level' : 'warning' } )
f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) f = _CreateFilterForTypes( opts, [ 'java' ] )
_assert_rejects( f, { 'text' : 'This is an unimportant taco', _assert_rejects( f, { 'text' : 'This is an unimportant taco',
'kind' : 'WARNING' } ) 'kind' : 'WARNING' } )
@ -82,7 +98,7 @@ def LevelWarnings_test():
def LevelErrors_test(): def LevelErrors_test():
opts = _JavaFilter( { 'level' : 'error' } ) opts = _JavaFilter( { 'level' : 'error' } )
f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) f = _CreateFilterForTypes( opts, [ 'java' ] )
_assert_accepts( f, { 'text' : 'This is an IMPORTANT taco', _assert_accepts( f, { 'text' : 'This is an IMPORTANT taco',
'kind' : 'WARNING' } ) 'kind' : 'WARNING' } )
@ -94,7 +110,7 @@ def MultipleFilterTypesTypeTest_test():
opts = _JavaFilter( { 'regex' : '.*taco.*', opts = _JavaFilter( { 'regex' : '.*taco.*',
'level' : 'warning' } ) 'level' : 'warning' } )
f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) f = _CreateFilterForTypes( opts, [ 'java' ] )
_assert_rejects( f, { 'text' : 'This is an unimportant taco', _assert_rejects( f, { 'text' : 'This is an unimportant taco',
'kind' : 'WARNING' } ) 'kind' : 'WARNING' } )
@ -110,7 +126,7 @@ def MergeMultipleFiletypes_test():
'java' : { 'regex' : '.*taco.*' }, 'java' : { 'regex' : '.*taco.*' },
'xml' : { 'regex' : '.*burrito.*' } } } 'xml' : { 'regex' : '.*burrito.*' } } }
f = DiagnosticFilter.from_filetype( opts, [ 'java', 'xml' ] ) f = _CreateFilterForTypes( opts, [ 'java', 'xml' ] )
_assert_rejects( f, 'This is a Taco' ) _assert_rejects( f, 'This is a Taco' )
_assert_rejects( f, 'This is a Burrito' ) _assert_rejects( f, 'This is a Burrito' )
@ -122,7 +138,7 @@ def CommaSeparatedFiletypes_test():
opts = { 'filter_diagnostics' : { opts = { 'filter_diagnostics' : {
'java,c,cs' : { 'regex' : '.*taco.*' } } } 'java,c,cs' : { 'regex' : '.*taco.*' } } }
f = DiagnosticFilter.from_filetype( opts, [ 'cs' ] ) f = _CreateFilterForTypes( opts, [ 'cs' ] )
_assert_rejects( f, 'This is a Taco' ) _assert_rejects( f, 'This is a Taco' )
_assert_accepts( f, 'This is a Burrito' ) _assert_accepts( f, 'This is a Burrito' )