#!/usr/bin/ruby
# encoding: utf-8
=begin LICENSE
[The "BSD licence"]
Copyright (c) 2009-2010 Kyle Yetter
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=end
module ANTLR3
module Profile
=begin rdoc ANTLR3::Profile::ParserEvents
ANTLR3::Profile::ParserEvents expands basic debugging events for use by
recognition code generated by ANTLR when called with the -profile
switch.
=end
module ParserEvents
include ANTLR3::Debug::ParserEvents
def self.included( klass )
super
if klass.is_a?( ::Class )
def klass.profile?
true
end
end
end
def initialize( stream, options = {} )
options[ :debug_listener ] ||= Profiler.new( self )
super( stream, options )
end
def already_parsed_rule?( rule )
@debug_listener.examine_rule_memoization( rule )
super
end
def profile
@debug_listener.profile
end
def memoize( rule, start_index, success )
@debug_listener.memoize( rule, rule_start_index, sucess )
super
end
end
class DataSet < ::Array
include ::Math
def total
inject( :+ )
end
def average
length > 0 ? ( total.to_f / length ) : 0
end
def variance
length.zero? and return( 0.0 )
mean = average
inject( 0.0 ) { |t, i| t + ( i - mean )**2 } / ( length - 1 )
end
def standard_deviation
sqrt( variance )
end
end
unless const_defined?( :Profile )
Profile = Struct.new(
:grammar_file, :parser_class, :top_rule,
:rule_invocations, :guessing_rule_invocations, :rule_invocation_depth,
:fixed_looks, :cyclic_looks, :syntactic_predicate_looks,
:memoization_cache_entries, :memoization_cache_hits,
:memoization_cache_misses, :tokens, :hidden_tokens,
:characters_matched, :hidden_characters_matched, :semantic_predicates,
:syntactic_predicates, :reported_errors
)
end
class Profile
def initialize
init_values = Array.new( self.class.members.length, 0 )
super( *init_values )
self.top_rule = self.parser_class = self.grammar_file = nil
self.fixed_looks = DataSet.new
self.cyclic_looks = DataSet.new
self.syntactic_predicate_looks = DataSet.new
end
def fixed_decisions
fixed_looks.length
end
def cyclic_decisions
cyclic_looks.length
end
def backtracking_decisions
syntactic_predicate_looks.length
end
def generate_report
report = '+' << '-' * 78 << "+\n"
report << '| ' << "ANTLR Rule Profile".center( 76 ) << " |\n"
report << '+' << '-' * 78 << "+\n"
report << "| Generated at #{ Time.now }".ljust( 78 ) << " |\n"
report << "| Profiled #{ parser_class.name }##{ top_rule }".ljust( 78 ) << " |\n"
report << "| Rule source generated from grammar file #{ grammar_file }".ljust( 78 ) << " |\n"
report << '+' << '-' * 78 << "+\n"
report << '| ' << "Rule Invocations".center( 76 ) << " |\n"
report << '+' << '-' * 68 << '+' << '-' * 9 << "+\n"
report << "| %-66s | %7i |\n" % [ "Total Invocations", rule_invocations ]
report << "| %-66s | %7i |\n" % [ "``Guessing'' Invocations", guessing_rule_invocations ]
report << "| %-66s | %7i |\n" % [ "Deepest Level of Invocation", rule_invocation_depth ]
report << '+' << '-' * 68 << '+' << '-' * 9 << "+\n"
report << '| ' << "Execution Events".center( 76 ) << " |\n"
report << '+' << '-' * 68 << '+' << '-' * 9 << "+\n"
report << "| %-66s | %7i |\n" % [ "Semantic Predicates Evaluated", semantic_predicates ]
report << "| %-66s | %7i |\n" % [ "Syntactic Predicates Evaluated", syntactic_predicates ]
report << "| %-66s | %7i |\n" % [ "Errors Reported", reported_errors ]
report << '+' << '-' * 68 << '+' << '-' * 9 << "+\n"
report << '| ' << "Token and Character Data".center( 76 ) << " |\n"
report << '+' << '-' * 68 << '+' << '-' * 9 << "+\n"
report << "| %-66s | %7i |\n" % [ "Tokens Consumed", tokens ]
report << "| %-66s | %7i |\n" % [ "Hidden Tokens Consumed", hidden_tokens ]
report << "| %-66s | %7i |\n" % [ "Characters Matched", characters_matched ]
report << "| %-66s | %7i |\n" % [ "Hidden Characters Matched", hidden_characters_matched ]
report << '+' << '-' * 68 << '+' << '-' * 9 << "+\n"
report << '| ' << "Memoization".center( 76 ) << " |\n"
report << '+' << '-' * 68 << '+' << '-' * 9 << "+\n"
report << "| %-66s | %7i |\n" % [ "Cache Entries", memoization_cache_entries ]
report << "| %-66s | %7i |\n" % [ "Cache Hits", memoization_cache_hits ]
report << "| %-66s | %7i |\n" % [ "Cache Misses", memoization_cache_misses ]
report << '+' << '-' * 68 << '+' << '-' * 9 << "+\n"
[
[ 'Fixed Lookahead (k)', fixed_looks ],
[ 'Arbitrary Lookahead (k)', cyclic_looks ],
[ 'Backtracking (Syntactic Predicate)', syntactic_predicate_looks ]
].each do |name, set|
mean, stdev = '%4.2f' % set.average, '%4.2f' % set.standard_deviation
report << '| ' << "#{ name } Decisions".center( 76 ) << " |\n"
report << '+' << '-' * 68 << '+' << '-' * 9 << "+\n"
report << "| %-66s | %7i |\n" % [ "Count", set.length ]
report << "| %-66s | %7i |\n" % [ "Minimum k", set.min ]
report << "| %-66s | %7i |\n" % [ "Maximum k", set.max ]
report << "| %-66s | %7s |\n" % [ "Average k", mean ]
report << "| %-66s | %7s |\n" % [ "Standard Deviation of k", stdev ]
report << '+' << '-' * 68 << '+' << '-' * 9 << "+\n"
end
return( report )
end
end
=begin rdoc ANTLR3::Profile::Profiler
When ANTLR is run with the -profile switch, it generates recognition
code that performs accounting about the decision logic performed while parsing
any given input. This information can be used to help refactor a slow grammar.
Profiler is an event-listener that performs all of the profiling accounting and
builds a simple report to present the various statistics.
=end
class Profiler
include Debug::EventListener
include Constants
PROTOCOL_VERSION = 2
attr_accessor :parser
attr_reader :rule_level
attr_reader :decision_level
# tracks the maximum look value for the current decision
# (maxLookaheadInCurrentDecision in java Profiler)
attr_reader :decision_look
# the last token consumed
# (lastTokenConsumed in java Profiler)
attr_reader :last_token
attr_reader :look_stack
attr_reader :profile
attr_accessor :output
def initialize( parser = nil, output = nil )
@parser = parser
@profile = nil
@rule_level = 0
@decision_level = 0
@decision_look = 0
@last_token = nil
@look_stack = []
@output = output
end
def commence
@profile = Profile.new
@rule_level = 0
@decision_level = 0
@decision_look = 0
@last_token = nil
@look_stack = []
end
def enter_rule( grammar_file_name, rule_name )
if @rule_level.zero?
commence
@profile.grammar_file = grammar_file_name
@profile.parser_class = @parser.class
@profile.top_rule = rule_name
end
@rule_level += 1
@profile.rule_invocations += 1
@profile.rule_invocation_depth < @rule_level and
@profile.rule_invocation_depth = @rule_level
end
def exit_rule( grammar_file_name, rule_name )
@rule_level -= 1
end
def examine_rule_memoization( rule )
stop_index = parser.rule_memoization( rule, @parser.input.index )
if stop_index == MEMO_RULE_UNKNOWN
@profile.memoization_cache_misses += 1
@profile.guessing_rule_invocations += 1
else
@profile.memoization_cache_hits += 1
end
end
def memoize( rule, start_index, success )
@profile.memoization_cache_entries += 1
end
def enter_decision( decision_number )
@decision_level += 1
starting_look_index = @parser.input.index
@look_stack << starting_look_index
end
def exit_decision( decision_number )
@look_stack.pop
@decision_level -= 1
if @parser.cyclic_decision? then
@profile.cyclic_looks << @decision_look
else @profile.fixed_looks << @decision_look
end
@parser.cyclic_decision = false
@decision_look = 0
end
def consume_token( token )
@last_token = token
end
def in_decision?
return( @decision_level > 0 )
end
def consume_hidden_token( token )
@last_token = token
end
def look( i, token )
in_decision? or return
starting_index = look_stack.last
input = @parser.input
this_ref_index = input.index
num_hidden = input.tokens( starting_index, this_ref_index ).count { |t| t.hidden? }
depth = i + this_ref_index - starting_index - num_hidden
if depth > @decision_look
@decision_look = depth
end
end
def end_backtrack( level, successful )
@profile.syntactic_predicate_looks << @decision_look
end
def recognition_exception( error )
@profile.reported_errors += 1
end
def semantic_predicate( result, predicate )
in_decision? and @profile.semantic_predicates += 1
end
def terminate
input = @parser.input
hidden_tokens = input.select { |token| token.hidden? }
@profile.hidden_tokens = hidden_tokens.length
@profile.tokens = input.tokens.length
@profile.hidden_characters_matched = hidden_tokens.inject( 0 ) do |count, token|
count + token.text.length rescue count
end
@profile.characters_matched = ( @last_token || input.tokens.last ).stop + 1 rescue 0
write_report
end
def write_report
@output << @profile.generate_report unless @output.nil?
rescue NoMethodError => error
if error.name.to_s == '<<'
warn( <<-END.strip! % [ __FILE__, __LINE__, @output ] )
[%s @ %s]: failed to write report to %p as it does not respond to :<<
END
else raise
end
rescue IOError => error
$stderr.puts( Util.tidy( <<-END ) % [ __FILE__, __LINE__, @output, error.class, error.message ] )
| [%s @ %s]: failed to write profile report to %p due to an IO Error:
| %s: %s
END
$stderr.puts( error.backtrace.map { |call| " - #{ call }" }.join( "\n" ) )
end
def report
@profile.generate_report
end
alias to_s report
end
end
end