├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main └── java │ └── tracehash │ ├── TraceHash.java │ └── internal │ ├── ArrayUtil.java │ ├── KeyStackTraceComponent.java │ ├── SOCoverSolver.java │ └── StackTraceElementComparator.java └── test └── scala └── Tests.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.swo 2 | *.swp 3 | lib_managed 4 | project/boot 5 | project/.boot 6 | project/.ivy 7 | full/project/boot 8 | src_managed 9 | target 10 | .DS_Store 11 | out 12 | tags 13 | *~ 14 | build.log 15 | .history 16 | .idea/inspectionProfiles/** 17 | .lib 18 | .idea 19 | .idea_modules 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alexander Konovalov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TraceHash 2 | `TraceHash` hashes your exceptions into exception signatures that formalize the intuitive notion of "exception sameness": exceptions with the same signature are normally considered "the same" (e.g. when filing bug reports). 3 | 4 | ## Usage 5 | 6 | ```scala 7 | tracehash.stackTraceHash(exception) 8 | // will produce something like 9 | // "SOE-b33ffcec6a101750802bcebecae59e6a657145aa" 10 | // or "IOOBE-1b4035e1d5b6023ecd1ef2673278057b5a3bb44c" 11 | ``` 12 | 13 | ## Motivation 14 | 15 | Say you are fuzzing a Java application, and find an `AssertionError`: 16 | 17 | ```scala 18 | java.lang.AssertionError: assertion failed: position error: position not set for Ident() # 5299 19 | at scala.Predef$.assert(Predef.scala:219) 20 | at dotty.tools.dotc.ast.Positioned.check$1(Positioned.scala:179) 21 | at dotty.tools.dotc.ast.Positioned.$anonfun$checkPos$4(Positioned.scala:203) 22 | at dotty.tools.dotc.ast.Positioned.$anonfun$checkPos$4$adapted(Positioned.scala:203) 23 | at scala.collection.immutable.List.foreach(List.scala:389) 24 | at dotty.tools.dotc.ast.Positioned.check$1(Positioned.scala:203) 25 | at dotty.tools.dotc.ast.Positioned.checkPos(Positioned.scala:216) 26 | .... 27 | ``` 28 | 29 | If the same `AssertionError` happens with a different input file, the two errors are probably related, and you should only file one issue for both of them. But what exactly do we mean by "same error"? What algorithm should we 30 | use to compare different exception traces? 31 | 32 | **Should we compare the entire stacktrace?** 33 | No, folklore and experience tells us that only the last few stacktrace entries are important. 34 | 35 | **Should we compare exception messages?** 36 | Unless we can inspect the code generating messages, we don't know which parts of the message stay constant and which depend on a particular fuzzer input or change non-deterministically. 37 | 38 | **Should we compare line numbers?** 39 | If someone changes one of the files appearing in the stacktrace without fixing the error, line numbers might change, but the error won't. Therefore, we should not take line numbers into account. 40 | 41 | **Should we compare file names?** 42 | File names are less important than class names, especially in Scala, where a single file can contain multiple classes. 43 | 44 | --- 45 | 46 | `TraceHash` would simplify the above exception down to: 47 | ```scala 48 | java.lang.AssertionError 49 | at scala.Predef$.assert 50 | at dotty.tools.dotc.ast.Positioned.check$1 51 | at dotty.tools.dotc.ast.Positioned.$anonfun$checkPos$4 52 | at dotty.tools.dotc.ast.Positioned.$anonfun$checkPos$4$adapted 53 | at scala.collection.immutable.List.foreach 54 | ``` 55 | and then hash it using SHA-1. 56 | 57 | ### Stack overflows 58 | 59 | Special care needs to be taken to simplify `StackOverflowException`, 60 | such as: 61 | 62 | ```scala 63 | java.lang.StackOverflowError 64 | at dotty.tools.dotc.core.Types$TypeProxy.superType(Types.scala:1460) 65 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:192) 66 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 67 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 68 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:182) 69 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 70 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 71 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:192) 72 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 73 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 74 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:178) 75 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 76 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 77 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:192) 78 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 79 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 80 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:182) 81 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 82 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 83 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:192) 84 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 85 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 86 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:178) 87 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 88 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 89 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:192) 90 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 91 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 92 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:182) 93 | ``` 94 | 95 | We can see that this stacktrace consists of a repeating fragment of 96 | length 11: 97 | ```scala 98 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 99 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 100 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:182) 101 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 102 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 103 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:192) 104 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 105 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 106 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1(TypeApplications.scala:178) 107 | at dotty.tools.dotc.util.Stats$.track(Stats.scala:35) 108 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension(TypeApplications.scala:171) 109 | ``` 110 | 111 | and a prefix of length 1: 112 | ```scala 113 | at dotty.tools.dotc.core.Types$TypeProxy.superType(Types.scala:1460) 114 | ``` 115 | 116 | Clearly, the prefix is not important, only the repeating fragment *is*. 117 | 118 | Note that looking at the very end of a `StackOveflowException` stacktrace, we can not tell how the repeating fragment started. For instance, let's imagine that our stacktrace ends in `d b a b c a b c a b c`. We can not tell if the repeating fragment is `a b c` or `b c a` or `c a b`. In order to produce consistent signatures, `TraceHash` sorts all possible options in lexicographic order. 119 | 120 | `TraceHash` would simplify the above exception stacktrace down to (modulo possible reordering of the entries as explained above): 121 | ```scala 122 | java.lang.StackOverflowError 123 | at dotty.tools.dotc.util.Stats$.track 124 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension 125 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1 126 | at dotty.tools.dotc.util.Stats$.track 127 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension 128 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1 129 | at dotty.tools.dotc.util.Stats$.track 130 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension 131 | at dotty.tools.dotc.core.TypeApplications$.$anonfun$typeParams$extension$1 132 | at dotty.tools.dotc.util.Stats$.track 133 | at dotty.tools.dotc.core.TypeApplications$.typeParams$extension 134 | ``` 135 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val common = Seq( 2 | organization := "com.alexknvl", 3 | version := "0.0.3-SNAPSHOT", 4 | scalaVersion := "2.12.6", 5 | libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % "test", 6 | libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.14.0" % "test") 7 | 8 | 9 | lazy val root = project.in(file(".")) 10 | .settings(common : _*) 11 | .settings( 12 | name := "tracehash", 13 | crossPaths := false, 14 | autoScalaLibrary := false) 15 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.1.4 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC13") 2 | -------------------------------------------------------------------------------- /src/main/java/tracehash/TraceHash.java: -------------------------------------------------------------------------------- 1 | package tracehash; 2 | 3 | import tracehash.internal.KeyStackTraceComponent; 4 | 5 | import java.lang.reflect.Method; 6 | import java.nio.charset.Charset; 7 | import java.nio.charset.UnsupportedCharsetException; 8 | import java.security.MessageDigest; 9 | import java.security.NoSuchAlgorithmException; 10 | import java.util.ArrayList; 11 | import java.util.Objects; 12 | 13 | public class TraceHash { 14 | public static Parameters DEFAULT_PARAMETERS = new Parameters(255, 2, 5, false); 15 | private static String NULL_STRING = "{null}"; 16 | 17 | public static class Parameters { 18 | private final int maxFragmentSize; 19 | private final int minFragmentCount; 20 | private final int nonSOESize; 21 | private final boolean noSynthetic; 22 | 23 | public Parameters(int maxFragmentSize, int minFragmentCount, int nonSOESize, boolean noSynthetic) { 24 | this.maxFragmentSize = maxFragmentSize; 25 | this.minFragmentCount = minFragmentCount; 26 | this.nonSOESize = nonSOESize; 27 | this.noSynthetic = noSynthetic; 28 | } 29 | } 30 | 31 | private static boolean isDefinitelySynthetic(String className, String methodName) { 32 | if (className == null || methodName == null) 33 | return false; 34 | 35 | Class cls; 36 | try { 37 | cls = Class.forName(className); 38 | } catch (ClassNotFoundException e) { 39 | return false; 40 | } 41 | 42 | final Method[] declaredMethods = cls.getDeclaredMethods(); 43 | if (declaredMethods.length == 0) return false; 44 | for (Method method : declaredMethods) { 45 | if (!method.getName().equals(methodName)) continue; 46 | if (!method.isSynthetic()) return false; 47 | } 48 | return true; 49 | } 50 | 51 | public static String hash(Parameters parameters, Throwable throwable, State state) { 52 | Objects.requireNonNull(parameters); 53 | Objects.requireNonNull(throwable); 54 | Objects.requireNonNull(state); 55 | 56 | final StackTraceElement[] stackTrace = throwable.getStackTrace(); 57 | final boolean isStackOverflow = throwable instanceof StackOverflowError; 58 | 59 | KeyStackTraceComponent.get(stackTrace, isStackOverflow, 60 | parameters.maxFragmentSize, 61 | parameters.minFragmentCount, 62 | state.keyStackTraceComponent); 63 | 64 | StringBuilder descriptionBuilder = new StringBuilder(); 65 | descriptionBuilder.append(throwable.getClass().getCanonicalName()).append(':'); 66 | 67 | int total = 0; 68 | int maxSize = isStackOverflow ? Integer.MAX_VALUE : parameters.nonSOESize; 69 | for (int i = 0; i < state.keyStackTraceComponent.length && total < maxSize; i++) { 70 | int index = state.keyStackTraceComponent.index + i; 71 | String className = stackTrace[index].getClassName(); 72 | String methodName = stackTrace[index].getMethodName(); 73 | 74 | if (parameters.noSynthetic && isDefinitelySynthetic(className, methodName)) 75 | continue; 76 | 77 | if (className == null) className = NULL_STRING; 78 | if (methodName == null) methodName = NULL_STRING; 79 | descriptionBuilder.append(className).append('/').append(methodName).append('|'); 80 | 81 | total += 1; 82 | } 83 | String hash = digest(state.messageDigest, state.charset, descriptionBuilder.toString()); 84 | 85 | StringBuilder resultBuilder = new StringBuilder(); 86 | String throwableName = throwable.getClass().getName(); 87 | for (int i = 0; i < throwableName.length(); i++) { 88 | char ch = throwableName.charAt(i); 89 | if (Character.isUpperCase(ch)) resultBuilder.append(ch); 90 | } 91 | resultBuilder.append('-').append(hash); 92 | return resultBuilder.toString(); 93 | } 94 | 95 | public static String hash(Parameters parameters, Throwable throwable) { 96 | Objects.requireNonNull(parameters); 97 | Objects.requireNonNull(throwable); 98 | 99 | State state; 100 | try { 101 | state = new State(); 102 | } catch (StateInitializationException e) { 103 | throw new Error(e); 104 | } 105 | return hash(parameters, throwable, state); 106 | } 107 | 108 | public static StackTraceElement[] principal(Parameters parameters, Throwable throwable) { 109 | Objects.requireNonNull(parameters); 110 | Objects.requireNonNull(throwable); 111 | 112 | State state; 113 | try { 114 | state = new State(); 115 | } catch (StateInitializationException e) { 116 | throw new Error(e); 117 | } 118 | 119 | final StackTraceElement[] stackTrace = throwable.getStackTrace(); 120 | final boolean isStackOverflow = throwable instanceof StackOverflowError; 121 | 122 | KeyStackTraceComponent.get(stackTrace, isStackOverflow, 123 | parameters.maxFragmentSize, 124 | parameters.minFragmentCount, 125 | state.keyStackTraceComponent); 126 | 127 | ArrayList builder = new ArrayList<>(); 128 | 129 | int total = 0; 130 | int maxSize = isStackOverflow ? Integer.MAX_VALUE : parameters.nonSOESize; 131 | for (int i = 0; i < state.keyStackTraceComponent.length && total < maxSize; i++) { 132 | int index = state.keyStackTraceComponent.index + i; 133 | String className = stackTrace[index].getClassName(); 134 | String methodName = stackTrace[index].getMethodName(); 135 | 136 | if (parameters.noSynthetic && isDefinitelySynthetic(className, methodName)) 137 | continue; 138 | 139 | builder.add(stackTrace[index]); 140 | 141 | total += 1; 142 | } 143 | 144 | return builder.toArray(new StackTraceElement[0]); 145 | } 146 | 147 | private static class StateInitializationException extends Exception { 148 | public StateInitializationException(Exception cause) { 149 | super(cause); 150 | } 151 | } 152 | 153 | public static class State { 154 | private final MessageDigest messageDigest; 155 | private final Charset charset; 156 | private final KeyStackTraceComponent.State keyStackTraceComponent; 157 | 158 | public State() throws StateInitializationException { 159 | try { 160 | messageDigest = MessageDigest.getInstance("SHA-1"); 161 | charset = Charset.forName("UTF-8"); 162 | } catch (NoSuchAlgorithmException | UnsupportedCharsetException e) { 163 | throw new StateInitializationException(e); 164 | } 165 | 166 | keyStackTraceComponent = new KeyStackTraceComponent.State(); 167 | } 168 | } 169 | 170 | private static char hexChar(int x) { 171 | if (x <= 9) return (char) (x + '0'); 172 | else return (char) ('a' + (x - 10)); 173 | } 174 | 175 | static String digest(MessageDigest md, Charset charset, String str) { 176 | md.update(str.getBytes(charset)); 177 | 178 | byte[] bytes = md.digest(); 179 | StringBuilder builder = new StringBuilder(); 180 | for (byte b : bytes) { 181 | builder.append(hexChar((b >> 4) & 0xF)); 182 | builder.append(hexChar(b & 0xF)); 183 | } 184 | return builder.toString(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/tracehash/internal/ArrayUtil.java: -------------------------------------------------------------------------------- 1 | package tracehash.internal; 2 | 3 | import java.util.Comparator; 4 | 5 | public class ArrayUtil { 6 | /** 7 | * Compares two subarrays for equality. 8 | * Allocation-less. Complexity is O(F). 9 | * @param array 10 | * @param start1 11 | * @param start2 12 | * @param length 13 | * @return 14 | */ 15 | public static boolean equalRange(A[] array, int start1, int start2, int length) { 16 | assert array != null : "array can not be null"; 17 | assert 0 <= start1 : "start1 must be non-negative"; 18 | assert 0 <= start2 : "start2 must be non-negative"; 19 | assert start1 + length <= array.length : "length is too high"; 20 | assert start2 + length <= array.length : "length is too high"; 21 | 22 | for (int i = 0; i < length; i++) { 23 | A s1 = array[start1 + i]; 24 | A s2 = array[start2 + i]; 25 | 26 | if (s1 == null) { 27 | if (s2 != null) return false; 28 | } else if (!s1.equals(s2)) return false; 29 | } 30 | return true; 31 | } 32 | 33 | /** 34 | * Compares two subarrays. 35 | * Allocation-less. Complexity is O(F), where F is the length. 36 | * Result is always either -1, 0, or 1. 37 | * @param array 38 | * @param start1 39 | * @param start2 40 | * @param length 41 | * @return 42 | * @see Comparable#compareTo(Object) 43 | */ 44 | public static int compareRange(A[] array, int start1, int start2, int length, Comparator comparator) { 45 | assert array != null : "array can not be null"; 46 | assert 0 <= start1 : "start1 must be non-negative"; 47 | assert 0 <= start2 : "start2 must be non-negative"; 48 | assert start1 + length <= array.length : "length is too high"; 49 | assert start2 + length <= array.length : "length is too high"; 50 | 51 | for (int i = 0; i < length; i++) { 52 | A s1 = array[start1 + i]; 53 | A s2 = array[start2 + i]; 54 | 55 | int r = comparator.compare(s1, s2); 56 | if (r != 0) return r; 57 | } 58 | return 0; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/tracehash/internal/KeyStackTraceComponent.java: -------------------------------------------------------------------------------- 1 | package tracehash.internal; 2 | 3 | public class KeyStackTraceComponent { 4 | public static void get(StackTraceElement[] stack, boolean stackOverflow, int maxFragmentLength, int minFragmentCount, State result) { 5 | assert stack != null : "stack can not be null"; 6 | assert result != null : "result can not be null"; 7 | assert maxFragmentLength > 0 : "maxFragmentLength must be positive"; 8 | assert minFragmentCount > 0 : "minFragmentCount must be positive"; 9 | 10 | if (stackOverflow && getSO(stack, maxFragmentLength, minFragmentCount, result)) 11 | return; 12 | 13 | result.index = 0; 14 | result.length = stack.length; 15 | } 16 | 17 | public static class State { 18 | private SOCoverSolver.Result cover = new SOCoverSolver.Result(); 19 | 20 | public int index = 0; 21 | public int length = 0; 22 | } 23 | 24 | private static StackTraceElementComparator comparator = new StackTraceElementComparator(); 25 | 26 | public static boolean getSO(StackTraceElement[] stack, int maxFragmentLength, int minFragmentCount, State result) { 27 | assert stack != null : "stack can not be null"; 28 | assert result != null : "result can not be null"; 29 | assert maxFragmentLength > 0 : "maxFragmentLength must be positive"; 30 | assert minFragmentCount > 0 : "minFragmentCount must be positive"; 31 | 32 | SOCoverSolver.solve(stack, maxFragmentLength, minFragmentCount, result.cover); 33 | 34 | if (result.cover.coverLength >= result.cover.fragmentLength * 2) { 35 | result.index = SOCoverSolver.findRepresentativeFragment(stack, result.cover.fragmentLength, comparator); 36 | result.length = result.cover.fragmentLength; 37 | return true; 38 | } else return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/tracehash/internal/SOCoverSolver.java: -------------------------------------------------------------------------------- 1 | package tracehash.internal; 2 | 3 | import java.util.Comparator; 4 | 5 | public class SOCoverSolver { 6 | public static class Result { 7 | int suffixLength; 8 | int fragmentLength; 9 | int coverLength; 10 | } 11 | 12 | /** 13 | * Allocation-less. Complexity = O(F^3 + F^2 * (S / F) * F) = O(S F^2), where F is the maxFragmentLength. 14 | * @param stack 15 | * @param maxFragmentLength 16 | * @param minFragmentCount 17 | * @param result 18 | */ 19 | public static void solve(A[] stack, int maxFragmentLength, int minFragmentCount, Result result) { 20 | assert stack != null : "stack can not be null"; 21 | assert result != null : "callback can not be null"; 22 | assert maxFragmentLength > 0 : "maxFragmentLength must be positive"; 23 | assert minFragmentCount > 0 : "minFragmentCount must be positive"; 24 | 25 | int resultCoverLength = 0; 26 | int resultFragmentLength = 0; 27 | int resultSuffixLength = 0; 28 | 29 | for (int suffixLength = 1; suffixLength <= maxFragmentLength; suffixLength++) { 30 | if (2 * suffixLength > stack.length) 31 | break; 32 | 33 | // [ 0 .. suffixLength-1] 34 | 35 | int bestCoverage = 0; 36 | int bestFragmentLength = 0; 37 | 38 | for (int fragmentLength = suffixLength; fragmentLength <= maxFragmentLength; fragmentLength++) { 39 | if (fragmentLength + suffixLength > stack.length) 40 | break; 41 | 42 | // [0 .. suffixLength-1] [suffixLength .. suffixLength+fragmentLength-1] 43 | // We verify that the candidate fragment ends with the suffix. 44 | // First element of the suffix within the fragment has index 45 | // suffixLength+fragmentLength-1 - (suffixLength-1) = fragmentLength 46 | 47 | int suffixStart = stack.length - suffixLength; 48 | int initialFragmentStart = stack.length - suffixLength - fragmentLength; 49 | 50 | if (!ArrayUtil.equalRange(stack, suffixStart, initialFragmentStart, suffixLength)) 51 | continue; 52 | 53 | // Now that we know that the fragment is good, we compute its coverage. 54 | int coverage = suffixLength + fragmentLength; 55 | int count = 1; 56 | while (coverage + fragmentLength <= stack.length) { 57 | // We can potentially fit one more fragment. 58 | 59 | int fragmentStart = stack.length - coverage - fragmentLength; 60 | 61 | if (ArrayUtil.equalRange(stack, initialFragmentStart, fragmentStart, fragmentLength)) { 62 | coverage += fragmentLength; 63 | count += 1; 64 | } else break; 65 | } 66 | 67 | if (count < minFragmentCount) continue; 68 | 69 | // Best fragment will have maximum coverage. 70 | if (bestCoverage < coverage) { 71 | bestCoverage = coverage; 72 | bestFragmentLength = fragmentLength; 73 | } 74 | } 75 | 76 | // Best cover has maximum coverage, minimum fragment length, and minimum suffix length. 77 | if (resultCoverLength < bestCoverage) { 78 | resultCoverLength = bestCoverage; 79 | resultFragmentLength = bestFragmentLength; 80 | resultSuffixLength = suffixLength; 81 | } else if (resultCoverLength == bestCoverage) { 82 | if (resultFragmentLength > bestFragmentLength) { 83 | resultFragmentLength = bestFragmentLength; 84 | resultSuffixLength = suffixLength; 85 | } else if (resultFragmentLength == bestFragmentLength) { 86 | if (resultSuffixLength > suffixLength) { 87 | resultSuffixLength = suffixLength; 88 | } 89 | } 90 | } 91 | } 92 | 93 | assert resultSuffixLength <= resultFragmentLength; 94 | assert resultSuffixLength + resultFragmentLength <= resultCoverLength; 95 | 96 | result.suffixLength = resultSuffixLength; 97 | result.fragmentLength = resultFragmentLength; 98 | result.coverLength = resultCoverLength; 99 | } 100 | 101 | /** 102 | * Allocation-less. Complexity is O(F). 103 | * @param stack 104 | * @param fragmentLength 105 | * @return 106 | */ 107 | public static int findRepresentativeFragment(A[] stack, int fragmentLength, Comparator comparator) { 108 | assert stack != null : "stack can not be null"; 109 | assert 2 * fragmentLength <= stack.length : "at least two fragments must fit into stack"; 110 | 111 | int best = stack.length - fragmentLength; 112 | for (int i = 1; i < fragmentLength; i++) { 113 | int candidate = stack.length - fragmentLength - i; 114 | int r = ArrayUtil.compareRange(stack, candidate, best, fragmentLength, comparator); 115 | if (r < 0) best = candidate; 116 | } 117 | 118 | return best; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/tracehash/internal/StackTraceElementComparator.java: -------------------------------------------------------------------------------- 1 | package tracehash.internal; 2 | 3 | import java.util.Comparator; 4 | 5 | public class StackTraceElementComparator implements Comparator { 6 | /** 7 | * Compares two strings using {@link String#compareTo}. 8 | * 9 | * Allocation-less. Complexity is O(N), where N is 10 | * the length of the smallest argument. 11 | * @param a the first string 12 | * @param b the second string 13 | * @return the result of comparison 14 | * @see String#compareTo(String) 15 | */ 16 | static int compareStrings(String a, String b) { 17 | if (a == null) { 18 | if (b != null) return -1; 19 | } else { 20 | if (b == null) return 1; 21 | int r = a.compareTo(b); 22 | if (r != 0) return r; 23 | } 24 | return 0; 25 | } 26 | 27 | @Override public int compare(StackTraceElement s1, StackTraceElement s2) { 28 | if (s1 == null) { 29 | if (s2 != null) return -1; 30 | } else { 31 | if (s2 == null) return 1; 32 | 33 | int r = compareStrings(s1.getClassName(), s2.getClassName()); 34 | if (r != 0) return r; 35 | 36 | r = compareStrings(s1.getMethodName(), s2.getMethodName()); 37 | if (r != 0) return r; 38 | 39 | r = compareStrings(s1.getFileName(), s2.getFileName()); 40 | if (r != 0) return r; 41 | 42 | if (s1.getLineNumber() < s2.getLineNumber()) return -1; 43 | if (s1.getLineNumber() > s2.getLineNumber()) return 1; 44 | } 45 | 46 | return 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/scala/Tests.scala: -------------------------------------------------------------------------------- 1 | package tracehash.internal 2 | 3 | import org.scalatest.{FunSuite, Matchers} 4 | import org.scalacheck.{Arbitrary, Gen} 5 | import org.scalatest.prop.GeneratorDrivenPropertyChecks 6 | import tracehash.TraceHash 7 | 8 | import scala.reflect.ClassTag 9 | 10 | class Tests extends FunSuite with Matchers with GeneratorDrivenPropertyChecks { 11 | implicit val arbStackTraceElement: Arbitrary[StackTraceElement] = Arbitrary { 12 | for { 13 | cn <- Gen.alphaLowerChar.map(_.toString) 14 | mn <- Gen.alphaLowerChar.map(_.toString) 15 | fn <- Gen.alphaLowerChar.map(_.toString) 16 | ln <- Gen.choose(0, 5) 17 | } yield new StackTraceElement(cn, mn, fn, ln) 18 | } 19 | 20 | // test("sha1") { 21 | // sha1("string") shouldEqual "ecb252044b5ea0f679ee78ec1a12904739e2904d" 22 | // } 23 | 24 | test("bestCover(_, 1)") { 25 | forAll { s: Array[StackTraceElement] => 26 | val c = new SOCoverSolver.Result 27 | SOCoverSolver.solve(s, 1, 1, c) 28 | } 29 | 30 | val a = new StackTraceElement("a", "a", "a", 0) 31 | val b = new StackTraceElement("b", "b", "b", 1) 32 | val c = new StackTraceElement("c", "c", "c", 2) 33 | val r = new SOCoverSolver.Result 34 | 35 | { 36 | SOCoverSolver.solve(Array(b, a, a, a, a), 1, 1, r) 37 | r.coverLength shouldEqual 4 38 | r.fragmentLength shouldEqual 1 39 | r.suffixLength shouldEqual 1 40 | } 41 | 42 | { 43 | SOCoverSolver.solve(Array(b, a, b, a, a), 1, 1, r) 44 | r.coverLength shouldEqual 2 45 | r.fragmentLength shouldEqual 1 46 | r.suffixLength shouldEqual 1 47 | } 48 | 49 | { 50 | SOCoverSolver.solve(Array(b, a, b, a, c), 1, 1, r) 51 | r.coverLength shouldEqual 0 52 | r.fragmentLength shouldEqual 0 53 | r.suffixLength shouldEqual 0 54 | } 55 | } 56 | 57 | test("bestCover(_, 2)") { 58 | forAll { s: Array[StackTraceElement] => 59 | val c = new SOCoverSolver.Result 60 | SOCoverSolver.solve(s, 2, 1, c) 61 | } 62 | 63 | val a = new StackTraceElement("a", "a", "a", 0) 64 | val b = new StackTraceElement("b", "b", "b", 1) 65 | val c = new StackTraceElement("c", "c", "c", 2) 66 | val r = new SOCoverSolver.Result 67 | 68 | { 69 | SOCoverSolver.solve(Array(b, a, a, a, a), 2, 1, r) 70 | r.coverLength shouldEqual 4 71 | r.fragmentLength shouldEqual 1 72 | r.suffixLength shouldEqual 1 73 | } 74 | 75 | { 76 | SOCoverSolver.solve(Array(b, a, b, a, a), 2, 1, r) 77 | r.coverLength shouldEqual 2 78 | r.fragmentLength shouldEqual 1 79 | r.suffixLength shouldEqual 1 80 | } 81 | 82 | { 83 | SOCoverSolver.solve(Array(b, a, b, a, c), 2, 1, r) 84 | r.coverLength shouldEqual 0 85 | r.fragmentLength shouldEqual 0 86 | r.suffixLength shouldEqual 0 87 | } 88 | 89 | { 90 | SOCoverSolver.solve(Array(b, a, b, a, b), 2, 1, r) 91 | r.coverLength shouldEqual 5 92 | r.fragmentLength shouldEqual 2 93 | r.suffixLength shouldEqual 1 94 | } 95 | 96 | { 97 | SOCoverSolver.solve(Array(b, b, a, b, a, b), 2, 1, r) 98 | r.coverLength shouldEqual 5 99 | r.fragmentLength shouldEqual 2 100 | r.suffixLength shouldEqual 1 101 | } 102 | } 103 | 104 | test("bestCover(_, 255, 2)") { 105 | val a = new StackTraceElement("a", "a", "a", 0) 106 | val b = new StackTraceElement("b", "b", "b", 1) 107 | val c = new StackTraceElement("c", "c", "c", 2) 108 | val r = new SOCoverSolver.Result 109 | 110 | { 111 | SOCoverSolver.solve(Array(b, b, a, b, a, b, a, b), 3, 2, r) 112 | r.coverLength shouldEqual 7 113 | r.fragmentLength shouldEqual 2 114 | r.suffixLength shouldEqual 1 115 | } 116 | 117 | { 118 | SOCoverSolver.solve(Array(b, b, a, b, a, b, a, b), 255, 2, r) 119 | r.coverLength shouldEqual 7 120 | r.fragmentLength shouldEqual 2 121 | r.suffixLength shouldEqual 1 122 | } 123 | } 124 | 125 | test("KeyStackTraceComponent") { 126 | val a = new StackTraceElement("a", "a", "a", 0) 127 | val b = new StackTraceElement("b", "b", "b", 1) 128 | val c = new StackTraceElement("c", "c", "c", 2) 129 | 130 | { 131 | val state = new KeyStackTraceComponent.State 132 | val stack = Array(b, b, a, b, a, b, a, b) 133 | KeyStackTraceComponent.getSO(stack, 255, 2, state) 134 | stack.slice(state.index, state.index + state.length) shouldEqual Array(a, b) 135 | } 136 | 137 | { 138 | val state = new KeyStackTraceComponent.State 139 | val stack = Array(b, b, a, b, c, a, b, c, a, b) 140 | KeyStackTraceComponent.getSO(stack, 255, 2, state) 141 | stack.slice(state.index, state.index + state.length) shouldEqual Array(a, b, c) 142 | } 143 | } 144 | 145 | final case class Mocked(stack: Array[StackTraceElement]) extends Throwable { 146 | override def getStackTrace: Array[StackTraceElement] = stack 147 | } 148 | 149 | final case class MockedSO(stack: Array[StackTraceElement]) extends StackOverflowError { 150 | override def getStackTrace: Array[StackTraceElement] = stack 151 | } 152 | 153 | test("TraceHash.principal") { 154 | val a = new StackTraceElement("a", "a", "a", 0) 155 | val b = new StackTraceElement("b", "b", "b", 1) 156 | val c = new StackTraceElement("c", "c", "c", 2) 157 | 158 | val params = new TraceHash.Parameters(255, 2, 3, false) 159 | 160 | { 161 | val stack = Array(b, b, a, b, a, b, a, b) 162 | TraceHash.principal(params, Mocked(stack)) shouldEqual Array(b, b, a) 163 | TraceHash.principal(params, MockedSO(stack)) shouldEqual Array(a, b) 164 | } 165 | 166 | { 167 | val stack = Array(b, b, a, b, c, a, b, c, a, b) 168 | TraceHash.principal(params, Mocked(stack)) shouldEqual Array(b, b, a) 169 | TraceHash.principal(params, MockedSO(stack)) shouldEqual Array(a, b, c) 170 | } 171 | } 172 | } 173 | --------------------------------------------------------------------------------