#!/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