001/*
002 * Copyright (C) 2009-2011 Mathias Doenitz
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.parboiled.parserunners;
018
019import org.parboiled.MatchHandler;
020import org.parboiled.MatcherContext;
021import org.parboiled.Rule;
022import org.parboiled.buffers.InputBuffer;
023import org.parboiled.common.Predicate;
024import org.parboiled.common.StringUtils;
025import org.parboiled.matchers.Matcher;
026import org.parboiled.matchervisitors.DoWithMatcherVisitor;
027import org.parboiled.support.ParsingResult;
028import org.parboiled.matchers.Matcher;
029import org.parboiled.support.ParsingResult;
030
031import java.text.DecimalFormat;
032import java.util.ArrayList;
033import java.util.Collections;
034import java.util.Comparator;
035import java.util.HashMap;
036import java.util.List;
037import java.util.Map;
038
039import static org.parboiled.common.Preconditions.checkArgNotNull;
040import static org.parboiled.common.Utils.humanize;
041
042/**
043 * <p>The ProfilingParseRunner is a special {@link ParseRunner} implementation that "watches" a parser digest a number
044 * of inputs and collects all sorts of statistical data on the what rules have matched how many times, the number
045 * of reincovations of rules at identical input locations, and so on.</p>
046 * <p>The ProfilingParseRunner is typically used during parser debugging and optimization, not in production.</p>
047 *
048 * @param <V>
049 */
050public class ProfilingParseRunner<V> extends AbstractParseRunner<V> implements MatchHandler {
051    private final Map<Rule, RuleReport> ruleReports = new HashMap<Rule, RuleReport>();
052    private int runMatches;
053    private int totalRuns;
054    private int totalMatches;
055    private int totalMismatches;
056    private int totalRematches;
057    private int totalRemismatches;
058    private long totalNanoTime;
059    private long timeCorrection;
060
061    private final DoWithMatcherVisitor.Action updateStatsAction = new DoWithMatcherVisitor.Action() {
062        public void process(Matcher matcher) {
063            RuleStats ruleStats = (RuleStats) matcher.getTag();
064            int rematches = 0, remismatches = 0;
065            for (Integer i : ruleStats.positionMatches.values()) {
066                if (i > 0) {
067                    rematches += i - 1;
068                } else if (i < 0) {
069                    remismatches += -(i + 1);
070                }
071            }
072            totalMatches += ruleStats.matches;
073            totalMismatches += ruleStats.mismatches;
074            totalRematches += rematches;
075            totalRemismatches += remismatches;
076            RuleReport ruleReport = ruleReports.get(matcher);
077            if (ruleReport == null) {
078                ruleReport = new RuleReport(matcher);
079                ruleReports.put(matcher, ruleReport);
080            }
081            ruleReport.update(ruleStats.matches, ruleStats.matchSubs, ruleStats.mismatches, ruleStats.mismatchSubs,
082                    rematches, ruleStats.rematchSubs, remismatches, ruleStats.remismatchSubs, ruleStats.nanoTime);
083        }
084    };
085
086    /**
087     * Creates a new ProfilingParseRunner instance for the given rule.
088     *
089     * @param rule the parser rule
090     */
091    public ProfilingParseRunner(Rule rule) {
092        super(rule);
093    }
094
095    public ParsingResult<V> run(InputBuffer inputBuffer) {
096        checkArgNotNull(inputBuffer, "inputBuffer");
097        resetValueStack();
098        totalRuns++;
099
100        MatcherContext<V> rootContext = createRootContext(inputBuffer, this, true);
101        rootContext.getMatcher().accept(new DoWithMatcherVisitor(new DoWithMatcherVisitor.Action() {
102            public void process(Matcher matcher) {
103                RuleStats ruleStats = (RuleStats) matcher.getTag();
104                if (ruleStats == null) {
105                    ruleStats = new RuleStats();
106                    matcher.setTag(ruleStats);
107                } else {
108                    ruleStats.clear();
109                }
110            }
111        }));
112
113        runMatches = 0;
114        long timeStamp = System.nanoTime() - timeCorrection;
115        boolean matched = rootContext.runMatcher();
116        totalNanoTime += System.nanoTime() - timeCorrection - timeStamp;
117
118        getRootMatcher().accept(new DoWithMatcherVisitor(updateStatsAction));
119        return createParsingResult(matched, rootContext);
120    }
121
122    public Report getReport() {
123        return new Report(totalRuns, totalMatches, totalMismatches, totalRematches, totalRemismatches,
124                totalNanoTime, new ArrayList<RuleReport>(ruleReports.values()));
125    }
126
127    public boolean match(MatcherContext<?> context) {
128        long timeStamp = System.nanoTime();
129        Matcher matcher = context.getMatcher();
130        RuleStats ruleStats = ((RuleStats) matcher.getTag());
131        int pos = context.getCurrentIndex();
132
133        int subMatches = -++runMatches;
134        int matchSubs = ruleStats.matchSubs;
135        int rematchSubs = ruleStats.rematchSubs;
136        int mismatchSubs = ruleStats.mismatchSubs;
137        int remismatchSubs = ruleStats.remismatchSubs;
138
139        long time = System.nanoTime();
140        timeCorrection += time - timeStamp;
141        timeStamp = time - timeCorrection;
142
143        boolean matched = matcher.match(context);
144
145        time = System.nanoTime();
146        ruleStats.nanoTime += time - timeCorrection - timeStamp;
147        timeStamp = time;
148
149        subMatches += runMatches;
150
151        Integer posMatches = ruleStats.positionMatches.get(pos);
152        if (matched) {
153            ruleStats.matches++;
154            ruleStats.matchSubs = matchSubs + subMatches;
155            if (posMatches == null) {
156                posMatches = 1;
157            } else if (posMatches > 0) {
158                posMatches++;
159                ruleStats.rematchSubs = rematchSubs + subMatches;
160            } else if (posMatches < 0) {
161                posMatches = 0;
162            }
163        } else {
164            ruleStats.mismatches++;
165            ruleStats.mismatchSubs = mismatchSubs + subMatches;
166            if (posMatches == null) {
167                posMatches = -1;
168            } else if (posMatches < 0) {
169                posMatches--;
170                ruleStats.remismatchSubs = remismatchSubs + subMatches;
171            } else if (posMatches > 0) {
172                posMatches = 0;
173            }
174        }
175        ruleStats.positionMatches.put(pos, posMatches);
176        timeCorrection += System.nanoTime() - timeStamp;
177        return matched;
178    }
179
180    private static class RuleStats {
181        private int matches;
182        private int mismatches;
183        private int matchSubs;
184        private int mismatchSubs;
185        private int rematchSubs;
186        private int remismatchSubs;
187        private long nanoTime;
188
189        // map Index -> matches at that position
190        // no entry for a position means that the rule was never tried for that position
191        // an entry n > 0 means that the rule matched n times
192        // an entry n < 0 means that the rule failed n times
193        // an entry of 0 for a position means that the rule matched as well as failed at the position (should happen
194        // only for "strange" action rules)
195        private final Map<Integer, Integer> positionMatches = new HashMap<Integer, Integer>();
196
197        private void clear() {
198            matches = 0;
199            mismatches = 0;
200            matchSubs = 0;
201            mismatchSubs = 0;
202            rematchSubs = 0;
203            remismatchSubs = 0;
204            nanoTime = 0;
205            positionMatches.clear();
206        }
207    }
208
209    public static class Report {
210        private final static DecimalFormat fmt = new DecimalFormat("0.###");
211
212        public static final Predicate<RuleReport> allRules = new Predicate<RuleReport>() {
213            public boolean apply(RuleReport rep) {
214                return true;
215            }
216        };
217
218        public static final Predicate<RuleReport> namedRules = new Predicate<RuleReport>() {
219            public boolean apply(RuleReport rep) {
220                return rep.getMatcher().hasCustomLabel();
221            }
222        };
223
224        public final int totalRuns;
225        public final int totalInvocations;
226        public final int totalMatches;
227        public final int totalMismatches;
228        public final double matchShare;
229        public final int reinvocations;
230        public final int rematches;
231        public final int remismatches;
232        public final double reinvocationShare;
233        public final long totalNanoTime;
234        public final List<RuleReport> ruleReports;
235
236        public Report(int totalRuns, int totalMatches, int totalMismatches, int rematches, int remismatches,
237                      long totalNanoTime, List<RuleReport> ruleReports) {
238            this.totalRuns = totalRuns;
239            this.totalInvocations = totalMatches + totalMismatches;
240            this.totalMatches = totalMatches;
241            this.totalMismatches = totalMismatches;
242            this.matchShare = ((double) totalMatches) / totalInvocations;
243            this.reinvocations = rematches + remismatches;
244            this.rematches = rematches;
245            this.remismatches = remismatches;
246            this.reinvocationShare = ((double) reinvocations) / totalInvocations;
247            this.totalNanoTime = totalNanoTime;
248            this.ruleReports = ruleReports;
249        }
250
251        public String print() {
252            StringBuilder sb = new StringBuilder();
253            sb.append("Profiling Report\n");
254            sb.append("----------------\n");
255            sb.append(printBasics());
256            sb.append("\n");
257            sb.append("Top 20 named rules by invocations:\n");
258            sb.append(sortByInvocations().printTopRules(20, namedRules));
259            sb.append("\n");
260            sb.append("Top 20 named rules by sub-invocations:\n");
261            sb.append(sortBySubInvocations().printTopRules(20, namedRules));
262            sb.append("\n");
263            sb.append("Top 20 named rules by re-invocations:\n");
264            sb.append(sortByReinvocations().printTopRules(20, namedRules));
265            sb.append("\n");
266            sb.append("Top 20 named rules by re-sub-invocations:\n");
267            sb.append(sortByResubinvocations().printTopRules(20, namedRules));
268            sb.append("\n");
269            sb.append("Top 20 named rules by re-mismatches:\n");
270            sb.append(sortByRemismatches().printTopRules(20, namedRules));
271            sb.append("\n");
272            sb.append("Top 20 named rules by re-sub-mismatches:\n");
273            sb.append(sortByResubmismatches().printTopRules(20, namedRules));
274            return sb.toString();
275        }
276
277        public String printBasics() {
278            StringBuilder sb = new StringBuilder();
279            sb.append(String.format("Runs                     : %,15d\n", totalRuns));
280            sb.append(String.format("Active rules             : %,15d\n", ruleReports.size()));
281            sb.append(String.format("Total net rule time      : %,15.3f s\n", totalNanoTime / 1000000000.0));
282            sb.append(String.format("Total rule invocations   : %,15d\n", totalInvocations));
283            sb.append(String.format("Total rule matches       : %,15d\n", totalMatches));
284            sb.append(String.format("Total rule mismatches    : %,15d\n", totalMismatches));
285            sb.append(String.format("Total match share        : %15.2f %%\n", 100.0 * matchShare));
286            sb.append(String.format("Rule re-invocations      : %,15d\n", reinvocations));
287            sb.append(String.format("Rule re-matches          : %,15d\n", rematches));
288            sb.append(String.format("Rule re-mismatches       : %,15d\n", remismatches));
289            sb.append(String.format("Rule re-invocation share : %15.2f %%\n", 100.0 * reinvocationShare));
290            return sb.toString();
291        }
292
293        public String printTopRules(int count, Predicate<RuleReport> filter) {
294            checkArgNotNull(filter, "filter");
295            StringBuilder sb = new StringBuilder();
296            sb.append(
297                    "Rule                           | Net-Time  |   Invocations   |     Matches     |   Mismatches    |   Time/Invoc.   | Match % |    Re-Invocs    |   Re-Matches    |   Re-Mismatch   |     Re-Invoc %    \n");
298            sb.append(
299                    "-------------------------------|-----------|-----------------|-----------------|-----------------|-----------------|---------|-----------------|-----------------|-----------------|-------------------\n");
300            for (int i = 0; i < Math.min(ruleReports.size(), count); i++) {
301                RuleReport rep = ruleReports.get(i);
302                if (!filter.apply(rep)) {
303                    count++;
304                    continue;
305                }
306                sb.append(String.format(
307                        "%-30s | %6.0f ms | %6s / %6s | %6s / %6s | %6s / %6s | %,12.0f ns | %6.2f%% | %6s / %6s | %6s / %6s | %6s / %6s | %6.2f%% / %6.2f%%\n",
308                        StringUtils.left(
309                                rep.getMatcher().toString() + ": " + rep.getMatcher().getClass().getSimpleName()
310                                        .replace("Matcher", ""), 30),
311                        rep.getNanoTime() / 1000000.0,
312                        humanize(rep.getInvocations()), humanize(rep.getInvocationSubs()),
313                        humanize(rep.getMatches()), humanize(rep.getMatchSubs()),
314                        humanize(rep.getMismatches()), humanize(rep.getMismatchSubs()),
315                        rep.getNanoTime() / (double) rep.getInvocations(),
316                        rep.getMatchShare() * 100,
317                        humanize(rep.getReinvocations()), humanize(rep.getReinvocationSubs()),
318                        humanize(rep.getRematches()), humanize(rep.getRematchSubs()),
319                        humanize(rep.getRemismatches()), humanize(rep.getRemismatchSubs()),
320                        rep.getReinvocationShare() * 100, rep.getReinvocationShare2() * 100
321                ));
322            }
323            return sb.toString();
324        }
325
326        public Report sortByInvocations() {
327            Collections.sort(ruleReports, new Comparator<RuleReport>() {
328                public int compare(RuleReport a, RuleReport b) {
329                    return intCompare(a.getInvocations(), b.getInvocations());
330                }
331            });
332            return this;
333        }
334
335        public Report sortBySubInvocations() {
336            Collections.sort(ruleReports, new Comparator<RuleReport>() {
337                public int compare(RuleReport a, RuleReport b) {
338                    return intCompare(a.getInvocationSubs(), b.getInvocationSubs());
339                }
340            });
341            return this;
342        }
343
344        public Report sortByTime() {
345            Collections.sort(ruleReports, new Comparator<RuleReport>() {
346                public int compare(RuleReport a, RuleReport b) {
347                    return longCompare(a.getNanoTime(), b.getNanoTime());
348                }
349            });
350            return this;
351        }
352
353        public Report sortByTimePerInvocation() {
354            Collections.sort(ruleReports, new Comparator<RuleReport>() {
355                public int compare(RuleReport a, RuleReport b) {
356                    return doubleCompare(a.getNanoTime() / (double) a.getInvocations(),
357                            b.getNanoTime() / (double) b.getInvocations());
358                }
359            });
360            return this;
361        }
362
363        public Report sortByMatches() {
364            Collections.sort(ruleReports, new Comparator<RuleReport>() {
365                public int compare(RuleReport a, RuleReport b) {
366                    return intCompare(a.getMatches(), b.getMatches());
367                }
368            });
369            return this;
370        }
371
372        public Report sortByMismatches() {
373            Collections.sort(ruleReports, new Comparator<RuleReport>() {
374                public int compare(RuleReport a, RuleReport b) {
375                    return intCompare(a.getMismatches(), b.getMismatches());
376                }
377            });
378            return this;
379        }
380
381        public Report sortByReinvocations() {
382            Collections.sort(ruleReports, new Comparator<RuleReport>() {
383                public int compare(RuleReport a, RuleReport b) {
384                    return intCompare(a.getReinvocations(), b.getReinvocations());
385                }
386            });
387            return this;
388        }
389
390        public Report sortByResubinvocations() {
391            Collections.sort(ruleReports, new Comparator<RuleReport>() {
392                public int compare(RuleReport a, RuleReport b) {
393                    return doubleCompare(a.getReinvocationSubs(), b.getReinvocationSubs());
394                }
395            });
396            return this;
397        }
398
399        public Report sortByRematches() {
400            Collections.sort(ruleReports, new Comparator<RuleReport>() {
401                public int compare(RuleReport a, RuleReport b) {
402                    return intCompare(a.getRematches(), b.getRematches());
403                }
404            });
405            return this;
406        }
407
408        public Report sortByRemismatches() {
409            Collections.sort(ruleReports, new Comparator<RuleReport>() {
410                public int compare(RuleReport a, RuleReport b) {
411                    return intCompare(a.getRemismatches(), b.getRemismatches());
412                }
413            });
414            return this;
415        }
416
417        public Report sortByResubmismatches() {
418            Collections.sort(ruleReports, new Comparator<RuleReport>() {
419                public int compare(RuleReport a, RuleReport b) {
420                    return doubleCompare(a.getRemismatchSubs(), b.getRemismatchSubs());
421                }
422            });
423            return this;
424        }
425
426        private int intCompare(int a, int b) {
427            return a < b ? 1 : a > b ? -1 : 0;
428        }
429
430        private int longCompare(long a, long b) {
431            return a < b ? 1 : a > b ? -1 : 0;
432        }
433
434        private int doubleCompare(double a, double b) {
435            return a < b ? 1 : a > b ? -1 : 0;
436        }
437    }
438
439    public static class RuleReport {
440        private final Matcher matcher;
441        private int matches;
442        private int matchSubs;
443        private int mismatches;
444        private int mismatchSubs;
445        private int rematches;
446        private int rematchSubs;
447        private int remismatches;
448        private int remismatchSubs;
449        private long nanoTime;
450
451        public RuleReport(Matcher matcher) {
452            this.matcher = matcher;
453        }
454
455        public Matcher getMatcher() { return matcher; }
456
457        public int getInvocations() { return matches + mismatches; }
458
459        public int getInvocationSubs() { return matchSubs + mismatchSubs; }
460
461        public int getMatches() { return matches; }
462
463        public int getMatchSubs() { return matchSubs; }
464
465        public int getMismatches() { return mismatches; }
466
467        public int getMismatchSubs() { return mismatchSubs; }
468
469        public double getMatchShare() { return ((double) matches) / getInvocations(); }
470
471        public double getMatchShare2() { return ((double) matchSubs) / getInvocationSubs(); }
472
473        public int getReinvocations() { return rematches + remismatches; }
474
475        public int getReinvocationSubs() { return rematchSubs + remismatchSubs; }
476
477        public int getRematches() { return rematches; }
478
479        public int getRematchSubs() { return rematchSubs; }
480
481        public int getRemismatches() { return remismatches; }
482
483        public int getRemismatchSubs() { return remismatchSubs; }
484
485        public double getReinvocationShare() { return ((double) getReinvocations()) / getInvocations(); }
486
487        public double getReinvocationShare2() { return ((double) getReinvocationSubs()) / getInvocationSubs(); }
488
489        public long getNanoTime() { return nanoTime; }
490
491        public void update(int matchesDelta, int matchSubsDelta,
492                           int mismatchesDelta, int mismatchSubsDelta,
493                           int rematchesDelta, int rematchSubsDelta,
494                           int remismatchesDelta, int remismatchSubsDelta,
495                           long nanoTimeDelta) {
496            matches += matchesDelta;
497            matchSubs += matchSubsDelta;
498            mismatches += mismatchesDelta;
499            mismatchSubs += mismatchSubsDelta;
500            rematches += rematchesDelta;
501            rematchSubs += rematchSubsDelta;
502            remismatches += remismatchesDelta;
503            remismatchSubs += remismatchSubsDelta;
504            nanoTime += nanoTimeDelta;
505        }
506    }
507}