View Javadoc
1   /*
2    * Copyright (C) 2010, Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.internal.storage.pack;
12  
13  import java.io.EOFException;
14  import java.io.IOException;
15  import java.io.OutputStream;
16  import java.util.zip.Deflater;
17  
18  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
19  import org.eclipse.jgit.errors.LargeObjectException;
20  import org.eclipse.jgit.errors.MissingObjectException;
21  import org.eclipse.jgit.lib.ObjectReader;
22  import org.eclipse.jgit.lib.ProgressMonitor;
23  import org.eclipse.jgit.storage.pack.PackConfig;
24  import org.eclipse.jgit.util.TemporaryBuffer;
25  
26  final class DeltaWindow {
27  	private static final boolean NEXT_RES = false;
28  	private static final boolean NEXT_SRC = true;
29  
30  	private final PackConfig config;
31  	private final DeltaCache deltaCache;
32  	private final ObjectReader reader;
33  	private final ProgressMonitor monitor;
34  	private final long bytesPerUnit;
35  	private long bytesProcessed;
36  
37  	/** Maximum number of bytes to admit to the window at once. */
38  	private final long maxMemory;
39  
40  	/** Maximum depth we should create for any delta chain. */
41  	private final int maxDepth;
42  
43  	private final ObjectToPack[] toSearch;
44  	private int cur;
45  	private int end;
46  
47  	/** Amount of memory we have loaded right now. */
48  	private long loaded;
49  
50  	// The object we are currently considering needs a lot of state:
51  
52  	/** Window entry of the object we are currently considering. */
53  	private DeltaWindowEntry res;
54  
55  	/** If we have chosen a base, the window entry it was created from. */
56  	private DeltaWindowEntry bestBase;
57  	private int deltaLen;
58  	private Object deltaBuf;
59  
60  	/** Used to compress cached deltas. */
61  	private Deflater deflater;
62  
63  	DeltaWindow(PackConfig pc, DeltaCache dc, ObjectReader or,
64  			ProgressMonitor pm, long bpu,
65  			ObjectToPack[] in, int beginIndex, int endIndex) {
66  		config = pc;
67  		deltaCache = dc;
68  		reader = or;
69  		monitor = pm;
70  		bytesPerUnit = bpu;
71  		toSearch = in;
72  		cur = beginIndex;
73  		end = endIndex;
74  
75  		maxMemory = Math.max(0, config.getDeltaSearchMemoryLimit());
76  		maxDepth = config.getMaxDeltaDepth();
77  		res = DeltaWindowEntry.createWindow(config.getDeltaSearchWindowSize());
78  	}
79  
80  	synchronized DeltaTask.Slice remaining() {
81  		int e = end;
82  		int halfRemaining = (e - cur) >>> 1;
83  		if (0 == halfRemaining)
84  			return null;
85  
86  		int split = e - halfRemaining;
87  		int h = toSearch[split].getPathHash();
88  
89  		// Attempt to split on the next path after the 50% split point.
90  		for (int n = split + 1; n < e; n++) {
91  			if (h != toSearch[n].getPathHash())
92  				return new DeltaTask.Slice(n, e);
93  		}
94  
95  		if (h != toSearch[cur].getPathHash()) {
96  			// Try to split on the path before the 50% split point.
97  			// Do not split the path currently being processed.
98  			for (int p = split - 1; cur < p; p--) {
99  				if (h != toSearch[p].getPathHash())
100 					return new DeltaTask.Slice(p + 1, e);
101 			}
102 		}
103 		return null;
104 	}
105 
106 	synchronized boolean tryStealWork(DeltaTask.Slice s) {
107 		if (s.beginIndex <= cur || end <= s.beginIndex)
108 			return false;
109 		end = s.beginIndex;
110 		return true;
111 	}
112 
113 	void search() throws IOException {
114 		try {
115 			for (;;) {
116 				ObjectToPack next;
117 				synchronized (this) {
118 					if (end <= cur)
119 						break;
120 					next = toSearch[cur++];
121 				}
122 				if (maxMemory != 0) {
123 					clear(res);
124 					final long need = estimateSize(next);
125 					DeltaWindowEntry n = res.next;
126 					for (; maxMemory < loaded + need && n != res; n = n.next)
127 						clear(n);
128 				}
129 				res.set(next);
130 				clearWindowOnTypeSwitch();
131 
132 				if (res.object.isEdge() || res.object.doNotAttemptDelta()) {
133 					// We don't actually want to make a delta for
134 					// them, just need to push them into the window
135 					// so they can be read by other objects.
136 					keepInWindow();
137 				} else {
138 					// Search for a delta for the current window slot.
139 					if (bytesPerUnit <= (bytesProcessed += next.getWeight())) {
140 						int d = (int) (bytesProcessed / bytesPerUnit);
141 						monitor.update(d);
142 						bytesProcessed -= d * bytesPerUnit;
143 					}
144 					searchInWindow();
145 				}
146 			}
147 		} finally {
148 			if (deflater != null)
149 				deflater.end();
150 		}
151 	}
152 
153 	private static long estimateSize(ObjectToPack ent) {
154 		return DeltaIndex.estimateIndexSize(ent.getWeight());
155 	}
156 
157 	private static long estimateIndexSize(DeltaWindowEntry ent) {
158 		if (ent.buffer == null)
159 			return estimateSize(ent.object);
160 
161 		int len = ent.buffer.length;
162 		return DeltaIndex.estimateIndexSize(len) - len;
163 	}
164 
165 	private void clearWindowOnTypeSwitch() {
166 		DeltaWindowEntry p = res.prev;
167 		if (!p.empty() && res.type() != p.type()) {
168 			for (; p != res; p = p.prev) {
169 				clear(p);
170 			}
171 		}
172 	}
173 
174 	private void clear(DeltaWindowEntry ent) {
175 		if (ent.index != null)
176 			loaded -= ent.index.getIndexSize();
177 		else if (ent.buffer != null)
178 			loaded -= ent.buffer.length;
179 		ent.set(null);
180 	}
181 
182 	private void searchInWindow() throws IOException {
183 		// Loop through the window backwards, considering every entry.
184 		// This lets us look at the bigger objects that came before.
185 		for (DeltaWindowEntry src = res.prev; src != res; src = src.prev) {
186 			if (src.empty())
187 				break;
188 			if (delta(src) /* == NEXT_SRC */)
189 				continue;
190 			bestBase = null;
191 			deltaBuf = null;
192 			return;
193 		}
194 
195 		// We couldn't find a suitable delta for this object, but it may
196 		// still be able to act as a base for another one.
197 		if (bestBase == null) {
198 			keepInWindow();
199 			return;
200 		}
201 
202 		// Select this best matching delta as the base for the object.
203 		//
204 		ObjectToPack srcObj = bestBase.object;
205 		ObjectToPack resObj = res.object;
206 		if (srcObj.isEdge()) {
207 			// The source (the delta base) is an edge object outside of the
208 			// pack. Its part of the common base set that the peer already
209 			// has on hand, so we don't want to send it. We have to store
210 			// an ObjectId and *NOT* an ObjectToPack for the base to ensure
211 			// the base isn't included in the outgoing pack file.
212 			resObj.setDeltaBase(srcObj.copy());
213 		} else {
214 			// The base is part of the pack we are sending, so it should be
215 			// a direct pointer to the base.
216 			resObj.setDeltaBase(srcObj);
217 		}
218 
219 		int depth = srcObj.getDeltaDepth() + 1;
220 		resObj.setDeltaDepth(depth);
221 		resObj.clearReuseAsIs();
222 		cacheDelta(srcObj, resObj);
223 
224 		if (depth < maxDepth) {
225 			// Reorder the window so that the best base will be tested
226 			// first for the next object, and the current object will
227 			// be the second candidate to consider before any others.
228 			res.makeNext(bestBase);
229 			res = bestBase.next;
230 		}
231 
232 		bestBase = null;
233 		deltaBuf = null;
234 	}
235 
236 	private boolean delta(DeltaWindowEntry src)
237 			throws IOException {
238 		// If the sizes are radically different, this is a bad pairing.
239 		if (res.size() < src.size() >>> 4)
240 			return NEXT_SRC;
241 
242 		int msz = deltaSizeLimit(src);
243 		if (msz <= 8) // Nearly impossible to fit useful delta.
244 			return NEXT_SRC;
245 
246 		// If we have to insert a lot to make this work, find another.
247 		if (res.size() - src.size() > msz)
248 			return NEXT_SRC;
249 
250 		DeltaIndex srcIndex;
251 		try {
252 			srcIndex = index(src);
253 		} catch (LargeObjectException tooBig) {
254 			// If the source is too big to work on, skip it.
255 			return NEXT_SRC;
256 		} catch (IOException notAvailable) {
257 			if (src.object.isEdge()) // Missing edges are OK.
258 				return NEXT_SRC;
259 			throw notAvailable;
260 		}
261 
262 		byte[] resBuf;
263 		try {
264 			resBuf = buffer(res);
265 		} catch (LargeObjectException tooBig) {
266 			// If its too big, move on to another item.
267 			return NEXT_RES;
268 		}
269 
270 		try {
271 			OutputStream delta = msz <= (8 << 10)
272 				? new ArrayStream(msz)
273 				: new TemporaryBuffer.Heap(msz);
274 			if (srcIndex.encode(delta, resBuf, msz))
275 				selectDeltaBase(src, delta);
276 		} catch (IOException deltaTooBig) {
277 			// Unlikely, encoder should see limit and return false.
278 		}
279 		return NEXT_SRC;
280 	}
281 
282 	private void selectDeltaBase(DeltaWindowEntry src, OutputStream delta) {
283 		bestBase = src;
284 
285 		if (delta instanceof ArrayStream) {
286 			ArrayStream a = (ArrayStream) delta;
287 			deltaBuf = a.buf;
288 			deltaLen = a.cnt;
289 		} else {
290 			TemporaryBuffer.Heap b = (TemporaryBuffer.Heap) delta;
291 			deltaBuf = b;
292 			deltaLen = (int) b.length();
293 		}
294 	}
295 
296 	private int deltaSizeLimit(DeltaWindowEntry src) {
297 		if (bestBase == null) {
298 			// Any delta should be no more than 50% of the original size
299 			// (for text files deflate of whole form should shrink 50%).
300 			int n = res.size() >>> 1;
301 
302 			// Evenly distribute delta size limits over allowed depth.
303 			// If src is non-delta (depth = 0), delta <= 50% of original.
304 			// If src is almost at limit (9/10), delta <= 10% of original.
305 			return n * (maxDepth - src.depth()) / maxDepth;
306 		}
307 
308 		// With a delta base chosen any new delta must be "better".
309 		// Retain the distribution described above.
310 		int d = bestBase.depth();
311 		int n = deltaLen;
312 
313 		// If src is whole (depth=0) and base is near limit (depth=9/10)
314 		// any delta using src can be 10x larger and still be better.
315 		//
316 		// If src is near limit (depth=9/10) and base is whole (depth=0)
317 		// a new delta dependent on src must be 1/10th the size.
318 		return n * (maxDepth - src.depth()) / (maxDepth - d);
319 	}
320 
321 	private void cacheDelta(ObjectToPack./../../../../org/eclipse/jgit/internal/storage/pack/ObjectToPack.html#ObjectToPack">ObjectToPack srcObj, ObjectToPack resObj) {
322 		if (deltaCache.canCache(deltaLen, srcObj, resObj)) {
323 			try {
324 				byte[] zbuf = new byte[deflateBound(deltaLen)];
325 				ZipStream zs = new ZipStream(deflater(), zbuf);
326 				if (deltaBuf instanceof byte[])
327 					zs.write((byte[]) deltaBuf, 0, deltaLen);
328 				else
329 					((TemporaryBuffer.Heap) deltaBuf).writeTo(zs, null);
330 				deltaBuf = null;
331 				int len = zs.finish();
332 
333 				resObj.setCachedDelta(deltaCache.cache(zbuf, len, deltaLen));
334 				resObj.setCachedSize(deltaLen);
335 			} catch (IOException | OutOfMemoryError err) {
336 				deltaCache.credit(deltaLen);
337 			}
338 		}
339 	}
340 
341 	private static int deflateBound(int insz) {
342 		return insz + ((insz + 7) >> 3) + ((insz + 63) >> 6) + 11;
343 	}
344 
345 	private void keepInWindow() {
346 		res = res.next;
347 	}
348 
349 	private DeltaIndex index(DeltaWindowEntry ent)
350 			throws MissingObjectException, IncorrectObjectTypeException,
351 			IOException, LargeObjectException {
352 		DeltaIndex idx = ent.index;
353 		if (idx == null) {
354 			checkLoadable(ent, estimateIndexSize(ent));
355 
356 			try {
357 				idx = new DeltaIndex(buffer(ent));
358 			} catch (OutOfMemoryError noMemory) {
359 				LargeObjectException.OutOfMemory e;
360 				e = new LargeObjectException.OutOfMemory(noMemory);
361 				e.setObjectId(ent.object);
362 				throw e;
363 			}
364 			if (maxMemory != 0)
365 				loaded += idx.getIndexSize() - idx.getSourceSize();
366 			ent.index = idx;
367 		}
368 		return idx;
369 	}
370 
371 	private byte[] buffer(DeltaWindowEntry ent) throws MissingObjectException,
372 			IncorrectObjectTypeException, IOException, LargeObjectException {
373 		byte[] buf = ent.buffer;
374 		if (buf == null) {
375 			checkLoadable(ent, ent.size());
376 
377 			buf = PackWriter.buffer(config, reader, ent.object);
378 			if (maxMemory != 0)
379 				loaded += buf.length;
380 			ent.buffer = buf;
381 		}
382 		return buf;
383 	}
384 
385 	private void checkLoadable(DeltaWindowEntry ent, long need) {
386 		if (maxMemory == 0)
387 			return;
388 
389 		DeltaWindowEntry n = res.next;
390 		for (; maxMemory < loaded + need; n = n.next) {
391 			clear(n);
392 			if (n == ent)
393 				throw new LargeObjectException.ExceedsLimit(
394 						maxMemory, loaded + need);
395 		}
396 	}
397 
398 	private Deflater deflater() {
399 		if (deflater == null)
400 			deflater = new Deflater(config.getCompressionLevel());
401 		else
402 			deflater.reset();
403 		return deflater;
404 	}
405 
406 	static final class ZipStream extends OutputStream {
407 		private final Deflater deflater;
408 
409 		private final byte[] zbuf;
410 
411 		private int outPtr;
412 
413 		ZipStream(Deflater deflater, byte[] zbuf) {
414 			this.deflater = deflater;
415 			this.zbuf = zbuf;
416 		}
417 
418 		int finish() throws IOException {
419 			deflater.finish();
420 			for (;;) {
421 				if (outPtr == zbuf.length)
422 					throw new EOFException();
423 
424 				int n = deflater.deflate(zbuf, outPtr, zbuf.length - outPtr);
425 				if (n == 0) {
426 					if (deflater.finished())
427 						return outPtr;
428 					throw new IOException();
429 				}
430 				outPtr += n;
431 			}
432 		}
433 
434 		@Override
435 		public void write(byte[] b, int off, int len) throws IOException {
436 			deflater.setInput(b, off, len);
437 			for (;;) {
438 				if (outPtr == zbuf.length)
439 					throw new EOFException();
440 
441 				int n = deflater.deflate(zbuf, outPtr, zbuf.length - outPtr);
442 				if (n == 0) {
443 					if (deflater.needsInput())
444 						break;
445 					throw new IOException();
446 				}
447 				outPtr += n;
448 			}
449 		}
450 
451 		@Override
452 		public void write(int b) throws IOException {
453 			throw new UnsupportedOperationException();
454 		}
455 	}
456 
457 	static final class ArrayStream extends OutputStream {
458 		final byte[] buf;
459 		int cnt;
460 
461 		ArrayStream(int max) {
462 			buf = new byte[max];
463 		}
464 
465 		@Override
466 		public void write(int b) throws IOException {
467 			if (cnt == buf.length)
468 				throw new IOException();
469 			buf[cnt++] = (byte) b;
470 		}
471 
472 		@Override
473 		public void write(byte[] b, int off, int len) throws IOException {
474 			if (len > buf.length - cnt)
475 				throw new IOException();
476 			System.arraycopy(b, off, buf, cnt, len);
477 			cnt += len;
478 		}
479 	}
480 }