View Javadoc
1   /*
2    * Copyright (C) 2010, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.diff;
45  
46  import java.io.EOFException;
47  import java.io.IOException;
48  import java.io.InputStream;
49  import java.util.Arrays;
50  
51  import org.eclipse.jgit.errors.MissingObjectException;
52  import org.eclipse.jgit.lib.ObjectLoader;
53  import org.eclipse.jgit.lib.ObjectStream;
54  
55  /**
56   * Index structure of lines/blocks in one file.
57   * <p>
58   * This structure can be used to compute an approximation of the similarity
59   * between two files. The index is used by
60   * {@link org.eclipse.jgit.diff.SimilarityRenameDetector} to compute scores
61   * between files.
62   * <p>
63   * To save space in memory, this index uses a space efficient encoding which
64   * will not exceed 1 MiB per instance. The index starts out at a smaller size
65   * (closer to 2 KiB), but may grow as more distinct blocks within the scanned
66   * file are discovered.
67   *
68   * @since 4.0
69   */
70  public class SimilarityIndex {
71  	/** A special {@link TableFullException} used in place of OutOfMemoryError. */
72  	public static final TableFullException
73  			TABLE_FULL_OUT_OF_MEMORY = new TableFullException();
74  
75  	/**
76  	 * Shift to apply before storing a key.
77  	 * <p>
78  	 * Within the 64 bit table record space, we leave the highest bit unset so
79  	 * all values are positive. The lower 32 bits to count bytes.
80  	 */
81  	private static final int KEY_SHIFT = 32;
82  
83  	/** Maximum value of the count field, also mask to extract the count. */
84  	private static final long MAX_COUNT = (1L << KEY_SHIFT) - 1;
85  
86  	/**
87  	 * Total amount of bytes hashed into the structure, including \n. This is
88  	 * usually the size of the file minus number of CRLF encounters.
89  	 */
90  	private long hashedCnt;
91  
92  	/** Number of non-zero entries in {@link #idHash}. */
93  	private int idSize;
94  
95  	/** {@link #idSize} that triggers {@link #idHash} to double in size. */
96  	private int idGrowAt;
97  
98  	/**
99  	 * Pairings of content keys and counters.
100 	 * <p>
101 	 * Slots in the table are actually two ints wedged into a single long. The
102 	 * upper 32 bits stores the content key, and the remaining lower bits stores
103 	 * the number of bytes associated with that key. Empty slots are denoted by
104 	 * 0, which cannot occur because the count cannot be 0. Values can only be
105 	 * positive, which we enforce during key addition.
106 	 */
107 	private long[] idHash;
108 
109 	/** {@code idHash.length == 1 << idHashBits}. */
110 	private int idHashBits;
111 
112 	/**
113 	 * Create a new similarity index for the given object
114 	 *
115 	 * @param obj
116 	 *            the object to hash
117 	 * @return similarity index for this object
118 	 * @throws java.io.IOException
119 	 *             file contents cannot be read from the repository.
120 	 * @throws org.eclipse.jgit.diff.SimilarityIndex.TableFullException
121 	 *             object hashing overflowed the storage capacity of the
122 	 *             SimilarityIndex.
123 	 */
124 	public static SimilarityIndex create(ObjectLoader obj) throws IOException,
125 			TableFullException {
126 		SimilarityIndex idx = new SimilarityIndex();
127 		idx.hash(obj);
128 		idx.sort();
129 		return idx;
130 	}
131 
132 	SimilarityIndex() {
133 		idHashBits = 8;
134 		idHash = new long[1 << idHashBits];
135 		idGrowAt = growAt(idHashBits);
136 	}
137 
138 	void hash(ObjectLoader obj) throws MissingObjectException, IOException,
139 			TableFullException {
140 		if (obj.isLarge()) {
141 			hashLargeObject(obj);
142 		} else {
143 			byte[] raw = obj.getCachedBytes();
144 			hash(raw, 0, raw.length);
145 		}
146 	}
147 
148 	private void hashLargeObject(ObjectLoader obj) throws IOException,
149 			TableFullException {
150 		boolean text;
151 		try (ObjectStream in1 = obj.openStream()) {
152 			text = !RawText.isBinary(in1);
153 		}
154 
155 		try (ObjectStream in2 = obj.openStream()) {
156 			hash(in2, in2.getSize(), text);
157 		}
158 	}
159 
160 	void hash(byte[] raw, int ptr, int end) throws TableFullException {
161 		final boolean text = !RawText.isBinary(raw);
162 		hashedCnt = 0;
163 		while (ptr < end) {
164 			int hash = 5381;
165 			int blockHashedCnt = 0;
166 			int start = ptr;
167 
168 			// Hash one line, or one block, whichever occurs first.
169 			do {
170 				int c = raw[ptr++] & 0xff;
171 				// Ignore CR in CRLF sequence if text
172 				if (text && c == '\r' && ptr < end && raw[ptr] == '\n')
173 					continue;
174 				blockHashedCnt++;
175 				if (c == '\n')
176 					break;
177 				hash = (hash << 5) + hash + c;
178 			} while (ptr < end && ptr - start < 64);
179 			hashedCnt += blockHashedCnt;
180 			add(hash, blockHashedCnt);
181 		}
182 	}
183 
184 	void hash(InputStream in, long remaining, boolean text) throws IOException,
185 			TableFullException {
186 		byte[] buf = new byte[4096];
187 		int ptr = 0;
188 		int cnt = 0;
189 
190 		while (0 < remaining) {
191 			int hash = 5381;
192 			int blockHashedCnt = 0;
193 
194 			// Hash one line, or one block, whichever occurs first.
195 			int n = 0;
196 			do {
197 				if (ptr == cnt) {
198 					ptr = 0;
199 					cnt = in.read(buf, 0, buf.length);
200 					if (cnt <= 0)
201 						throw new EOFException();
202 				}
203 
204 				n++;
205 				int c = buf[ptr++] & 0xff;
206 				// Ignore CR in CRLF sequence if text
207 				if (text && c == '\r' && ptr < cnt && buf[ptr] == '\n')
208 					continue;
209 				blockHashedCnt++;
210 				if (c == '\n')
211 					break;
212 				hash = (hash << 5) + hash + c;
213 			} while (n < 64 && n < remaining);
214 			hashedCnt += blockHashedCnt;
215 			add(hash, blockHashedCnt);
216 			remaining -= n;
217 		}
218 	}
219 
220 	/**
221 	 * Sort the internal table so it can be used for efficient scoring.
222 	 * <p>
223 	 * Once sorted, additional lines/blocks cannot be added to the index.
224 	 */
225 	void sort() {
226 		// Sort the array. All of the empty space will wind up at the front,
227 		// because we forced all of the keys to always be positive. Later
228 		// we only work with the back half of the array.
229 		//
230 		Arrays.sort(idHash);
231 	}
232 
233 	/**
234 	 * Compute the similarity score between this index and another.
235 	 * <p>
236 	 * A region of a file is defined as a line in a text file or a fixed-size
237 	 * block in a binary file. To prepare an index, each region in the file is
238 	 * hashed; the values and counts of hashes are retained in a sorted table.
239 	 * Define the similarity fraction F as the count of matching regions
240 	 * between the two files divided between the maximum count of regions in
241 	 * either file. The similarity score is F multiplied by the maxScore
242 	 * constant, yielding a range [0, maxScore]. It is defined as maxScore for
243 	 * the degenerate case of two empty files.
244 	 * <p>
245 	 * The similarity score is symmetrical; i.e. a.score(b) == b.score(a).
246 	 *
247 	 * @param dst
248 	 *            the other index
249 	 * @param maxScore
250 	 *            the score representing a 100% match
251 	 * @return the similarity score
252 	 */
253 	public int score(SimilarityIndex dst, int maxScore) {
254 		long max = Math.max(hashedCnt, dst.hashedCnt);
255 		if (max == 0)
256 			return maxScore;
257 		return (int) ((common(dst) * maxScore) / max);
258 	}
259 
260 	long common(SimilarityIndex dst) {
261 		return common(this, dst);
262 	}
263 
264 	private static long common(SimilarityIndex./../../org/eclipse/jgit/diff/SimilarityIndex.html#SimilarityIndex">SimilarityIndex src, SimilarityIndex dst) {
265 		int srcIdx = src.packedIndex(0);
266 		int dstIdx = dst.packedIndex(0);
267 		long[] srcHash = src.idHash;
268 		long[] dstHash = dst.idHash;
269 		return common(srcHash, srcIdx, dstHash, dstIdx);
270 	}
271 
272 	private static long common(long[] srcHash, int srcIdx, //
273 			long[] dstHash, int dstIdx) {
274 		if (srcIdx == srcHash.length || dstIdx == dstHash.length)
275 			return 0;
276 
277 		long common = 0;
278 		int srcKey = keyOf(srcHash[srcIdx]);
279 		int dstKey = keyOf(dstHash[dstIdx]);
280 
281 		for (;;) {
282 			if (srcKey == dstKey) {
283 				common += Math.min(countOf(srcHash[srcIdx]),
284 						countOf(dstHash[dstIdx]));
285 
286 				if (++srcIdx == srcHash.length)
287 					break;
288 				srcKey = keyOf(srcHash[srcIdx]);
289 
290 				if (++dstIdx == dstHash.length)
291 					break;
292 				dstKey = keyOf(dstHash[dstIdx]);
293 
294 			} else if (srcKey < dstKey) {
295 				// Regions of src which do not appear in dst.
296 				if (++srcIdx == srcHash.length)
297 					break;
298 				srcKey = keyOf(srcHash[srcIdx]);
299 
300 			} else /* if (dstKey < srcKey) */{
301 				// Regions of dst which do not appear in src.
302 				if (++dstIdx == dstHash.length)
303 					break;
304 				dstKey = keyOf(dstHash[dstIdx]);
305 			}
306 		}
307 
308 		return common;
309 	}
310 
311 	// Testing only
312 	int size() {
313 		return idSize;
314 	}
315 
316 	// Testing only
317 	int key(int idx) {
318 		return keyOf(idHash[packedIndex(idx)]);
319 	}
320 
321 	// Testing only
322 	long count(int idx) {
323 		return countOf(idHash[packedIndex(idx)]);
324 	}
325 
326 	// Brute force approach only for testing.
327 	int findIndex(int key) {
328 		for (int i = 0; i < idSize; i++)
329 			if (key(i) == key)
330 				return i;
331 		return -1;
332 	}
333 
334 	private int packedIndex(int idx) {
335 		return (idHash.length - idSize) + idx;
336 	}
337 
338 	void add(int key, int cnt) throws TableFullException {
339 		key = (key * 0x9e370001) >>> 1; // Mix bits and ensure not negative.
340 
341 		int j = slot(key);
342 		for (;;) {
343 			long v = idHash[j];
344 			if (v == 0) {
345 				// Empty slot in the table, store here.
346 				if (idGrowAt <= idSize) {
347 					grow();
348 					j = slot(key);
349 					continue;
350 				}
351 				idHash[j] = pair(key, cnt);
352 				idSize++;
353 				return;
354 
355 			} else if (keyOf(v) == key) {
356 				// Same key, increment the counter. If it overflows, fail
357 				// indexing to prevent the key from being impacted.
358 				//
359 				idHash[j] = pair(key, countOf(v) + cnt);
360 				return;
361 
362 			} else if (++j >= idHash.length) {
363 				j = 0;
364 			}
365 		}
366 	}
367 
368 	private static long pair(int key, long cnt) throws TableFullException {
369 		if (MAX_COUNT < cnt)
370 			throw new TableFullException();
371 		return (((long) key) << KEY_SHIFT) | cnt;
372 	}
373 
374 	private int slot(int key) {
375 		// We use 31 - idHashBits because the upper bit was already forced
376 		// to be 0 and we want the remaining high bits to be used as the
377 		// table slot.
378 		//
379 		return key >>> (31 - idHashBits);
380 	}
381 
382 	private static int growAt(int idHashBits) {
383 		return (1 << idHashBits) * (idHashBits - 3) / idHashBits;
384 	}
385 
386 	private void grow() throws TableFullException {
387 		if (idHashBits == 30)
388 			throw new TableFullException();
389 
390 		long[] oldHash = idHash;
391 		int oldSize = idHash.length;
392 
393 		idHashBits++;
394 		idGrowAt = growAt(idHashBits);
395 
396 		try {
397 			idHash = new long[1 << idHashBits];
398 		} catch (OutOfMemoryError noMemory) {
399 			throw TABLE_FULL_OUT_OF_MEMORY;
400 		}
401 
402 		for (int i = 0; i < oldSize; i++) {
403 			long v = oldHash[i];
404 			if (v != 0) {
405 				int j = slot(keyOf(v));
406 				while (idHash[j] != 0)
407 					if (++j >= idHash.length)
408 						j = 0;
409 				idHash[j] = v;
410 			}
411 		}
412 	}
413 
414 	private static int keyOf(long v) {
415 		return (int) (v >>> KEY_SHIFT);
416 	}
417 
418 	private static long countOf(long v) {
419 		return v & MAX_COUNT;
420 	}
421 
422 	/** Thrown by {@code create()} when file is too large. */
423 	public static class TableFullException extends Exception {
424 		private static final long serialVersionUID = 1L;
425 	}
426 }