##// END OF EJS Templates
deltas: at a `target_rev` parameter to finddeltainfo...
marmoute -
r48249:93f4e183 default
parent child Browse files
Show More
@@ -1,1143 +1,1147 b''
1 1 # revlogdeltas.py - Logic around delta computation for revlog
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 4 # Copyright 2018 Octobus <contact@octobus.net>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8 """Helper class to compute deltas stored inside revlogs"""
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import collections
13 13 import struct
14 14
15 15 # import stuff from node for others to import from revlog
16 16 from ..node import nullrev
17 17 from ..i18n import _
18 18 from ..pycompat import getattr
19 19
20 20 from .constants import (
21 21 COMP_MODE_DEFAULT,
22 22 COMP_MODE_INLINE,
23 23 COMP_MODE_PLAIN,
24 24 REVIDX_ISCENSORED,
25 25 REVIDX_RAWTEXT_CHANGING_FLAGS,
26 26 )
27 27
28 28 from ..thirdparty import attr
29 29
30 30 from .. import (
31 31 error,
32 32 mdiff,
33 33 util,
34 34 )
35 35
36 36 from . import flagutil
37 37
38 38 # maximum <delta-chain-data>/<revision-text-length> ratio
39 39 LIMIT_DELTA2TEXT = 2
40 40
41 41
42 42 class _testrevlog(object):
43 43 """minimalist fake revlog to use in doctests"""
44 44
45 45 def __init__(self, data, density=0.5, mingap=0, snapshot=()):
46 46 """data is an list of revision payload boundaries"""
47 47 self._data = data
48 48 self._srdensitythreshold = density
49 49 self._srmingapsize = mingap
50 50 self._snapshot = set(snapshot)
51 51 self.index = None
52 52
53 53 def start(self, rev):
54 54 if rev == nullrev:
55 55 return 0
56 56 if rev == 0:
57 57 return 0
58 58 return self._data[rev - 1]
59 59
60 60 def end(self, rev):
61 61 if rev == nullrev:
62 62 return 0
63 63 return self._data[rev]
64 64
65 65 def length(self, rev):
66 66 return self.end(rev) - self.start(rev)
67 67
68 68 def __len__(self):
69 69 return len(self._data)
70 70
71 71 def issnapshot(self, rev):
72 72 if rev == nullrev:
73 73 return True
74 74 return rev in self._snapshot
75 75
76 76
77 77 def slicechunk(revlog, revs, targetsize=None):
78 78 """slice revs to reduce the amount of unrelated data to be read from disk.
79 79
80 80 ``revs`` is sliced into groups that should be read in one time.
81 81 Assume that revs are sorted.
82 82
83 83 The initial chunk is sliced until the overall density (payload/chunks-span
84 84 ratio) is above `revlog._srdensitythreshold`. No gap smaller than
85 85 `revlog._srmingapsize` is skipped.
86 86
87 87 If `targetsize` is set, no chunk larger than `targetsize` will be yield.
88 88 For consistency with other slicing choice, this limit won't go lower than
89 89 `revlog._srmingapsize`.
90 90
91 91 If individual revisions chunk are larger than this limit, they will still
92 92 be raised individually.
93 93
94 94 >>> data = [
95 95 ... 5, #00 (5)
96 96 ... 10, #01 (5)
97 97 ... 12, #02 (2)
98 98 ... 12, #03 (empty)
99 99 ... 27, #04 (15)
100 100 ... 31, #05 (4)
101 101 ... 31, #06 (empty)
102 102 ... 42, #07 (11)
103 103 ... 47, #08 (5)
104 104 ... 47, #09 (empty)
105 105 ... 48, #10 (1)
106 106 ... 51, #11 (3)
107 107 ... 74, #12 (23)
108 108 ... 85, #13 (11)
109 109 ... 86, #14 (1)
110 110 ... 91, #15 (5)
111 111 ... ]
112 112 >>> revlog = _testrevlog(data, snapshot=range(16))
113 113
114 114 >>> list(slicechunk(revlog, list(range(16))))
115 115 [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]]
116 116 >>> list(slicechunk(revlog, [0, 15]))
117 117 [[0], [15]]
118 118 >>> list(slicechunk(revlog, [0, 11, 15]))
119 119 [[0], [11], [15]]
120 120 >>> list(slicechunk(revlog, [0, 11, 13, 15]))
121 121 [[0], [11, 13, 15]]
122 122 >>> list(slicechunk(revlog, [1, 2, 3, 5, 8, 10, 11, 14]))
123 123 [[1, 2], [5, 8, 10, 11], [14]]
124 124
125 125 Slicing with a maximum chunk size
126 126 >>> list(slicechunk(revlog, [0, 11, 13, 15], targetsize=15))
127 127 [[0], [11], [13], [15]]
128 128 >>> list(slicechunk(revlog, [0, 11, 13, 15], targetsize=20))
129 129 [[0], [11], [13, 15]]
130 130
131 131 Slicing involving nullrev
132 132 >>> list(slicechunk(revlog, [-1, 0, 11, 13, 15], targetsize=20))
133 133 [[-1, 0], [11], [13, 15]]
134 134 >>> list(slicechunk(revlog, [-1, 13, 15], targetsize=5))
135 135 [[-1], [13], [15]]
136 136 """
137 137 if targetsize is not None:
138 138 targetsize = max(targetsize, revlog._srmingapsize)
139 139 # targetsize should not be specified when evaluating delta candidates:
140 140 # * targetsize is used to ensure we stay within specification when reading,
141 141 densityslicing = getattr(revlog.index, 'slicechunktodensity', None)
142 142 if densityslicing is None:
143 143 densityslicing = lambda x, y, z: _slicechunktodensity(revlog, x, y, z)
144 144 for chunk in densityslicing(
145 145 revs, revlog._srdensitythreshold, revlog._srmingapsize
146 146 ):
147 147 for subchunk in _slicechunktosize(revlog, chunk, targetsize):
148 148 yield subchunk
149 149
150 150
151 151 def _slicechunktosize(revlog, revs, targetsize=None):
152 152 """slice revs to match the target size
153 153
154 154 This is intended to be used on chunk that density slicing selected by that
155 155 are still too large compared to the read garantee of revlog. This might
156 156 happens when "minimal gap size" interrupted the slicing or when chain are
157 157 built in a way that create large blocks next to each other.
158 158
159 159 >>> data = [
160 160 ... 3, #0 (3)
161 161 ... 5, #1 (2)
162 162 ... 6, #2 (1)
163 163 ... 8, #3 (2)
164 164 ... 8, #4 (empty)
165 165 ... 11, #5 (3)
166 166 ... 12, #6 (1)
167 167 ... 13, #7 (1)
168 168 ... 14, #8 (1)
169 169 ... ]
170 170
171 171 == All snapshots cases ==
172 172 >>> revlog = _testrevlog(data, snapshot=range(9))
173 173
174 174 Cases where chunk is already small enough
175 175 >>> list(_slicechunktosize(revlog, [0], 3))
176 176 [[0]]
177 177 >>> list(_slicechunktosize(revlog, [6, 7], 3))
178 178 [[6, 7]]
179 179 >>> list(_slicechunktosize(revlog, [0], None))
180 180 [[0]]
181 181 >>> list(_slicechunktosize(revlog, [6, 7], None))
182 182 [[6, 7]]
183 183
184 184 cases where we need actual slicing
185 185 >>> list(_slicechunktosize(revlog, [0, 1], 3))
186 186 [[0], [1]]
187 187 >>> list(_slicechunktosize(revlog, [1, 3], 3))
188 188 [[1], [3]]
189 189 >>> list(_slicechunktosize(revlog, [1, 2, 3], 3))
190 190 [[1, 2], [3]]
191 191 >>> list(_slicechunktosize(revlog, [3, 5], 3))
192 192 [[3], [5]]
193 193 >>> list(_slicechunktosize(revlog, [3, 4, 5], 3))
194 194 [[3], [5]]
195 195 >>> list(_slicechunktosize(revlog, [5, 6, 7, 8], 3))
196 196 [[5], [6, 7, 8]]
197 197 >>> list(_slicechunktosize(revlog, [0, 1, 2, 3, 4, 5, 6, 7, 8], 3))
198 198 [[0], [1, 2], [3], [5], [6, 7, 8]]
199 199
200 200 Case with too large individual chunk (must return valid chunk)
201 201 >>> list(_slicechunktosize(revlog, [0, 1], 2))
202 202 [[0], [1]]
203 203 >>> list(_slicechunktosize(revlog, [1, 3], 1))
204 204 [[1], [3]]
205 205 >>> list(_slicechunktosize(revlog, [3, 4, 5], 2))
206 206 [[3], [5]]
207 207
208 208 == No Snapshot cases ==
209 209 >>> revlog = _testrevlog(data)
210 210
211 211 Cases where chunk is already small enough
212 212 >>> list(_slicechunktosize(revlog, [0], 3))
213 213 [[0]]
214 214 >>> list(_slicechunktosize(revlog, [6, 7], 3))
215 215 [[6, 7]]
216 216 >>> list(_slicechunktosize(revlog, [0], None))
217 217 [[0]]
218 218 >>> list(_slicechunktosize(revlog, [6, 7], None))
219 219 [[6, 7]]
220 220
221 221 cases where we need actual slicing
222 222 >>> list(_slicechunktosize(revlog, [0, 1], 3))
223 223 [[0], [1]]
224 224 >>> list(_slicechunktosize(revlog, [1, 3], 3))
225 225 [[1], [3]]
226 226 >>> list(_slicechunktosize(revlog, [1, 2, 3], 3))
227 227 [[1], [2, 3]]
228 228 >>> list(_slicechunktosize(revlog, [3, 5], 3))
229 229 [[3], [5]]
230 230 >>> list(_slicechunktosize(revlog, [3, 4, 5], 3))
231 231 [[3], [4, 5]]
232 232 >>> list(_slicechunktosize(revlog, [5, 6, 7, 8], 3))
233 233 [[5], [6, 7, 8]]
234 234 >>> list(_slicechunktosize(revlog, [0, 1, 2, 3, 4, 5, 6, 7, 8], 3))
235 235 [[0], [1, 2], [3], [5], [6, 7, 8]]
236 236
237 237 Case with too large individual chunk (must return valid chunk)
238 238 >>> list(_slicechunktosize(revlog, [0, 1], 2))
239 239 [[0], [1]]
240 240 >>> list(_slicechunktosize(revlog, [1, 3], 1))
241 241 [[1], [3]]
242 242 >>> list(_slicechunktosize(revlog, [3, 4, 5], 2))
243 243 [[3], [5]]
244 244
245 245 == mixed case ==
246 246 >>> revlog = _testrevlog(data, snapshot=[0, 1, 2])
247 247 >>> list(_slicechunktosize(revlog, list(range(9)), 5))
248 248 [[0, 1], [2], [3, 4, 5], [6, 7, 8]]
249 249 """
250 250 assert targetsize is None or 0 <= targetsize
251 251 startdata = revlog.start(revs[0])
252 252 enddata = revlog.end(revs[-1])
253 253 fullspan = enddata - startdata
254 254 if targetsize is None or fullspan <= targetsize:
255 255 yield revs
256 256 return
257 257
258 258 startrevidx = 0
259 259 endrevidx = 1
260 260 iterrevs = enumerate(revs)
261 261 next(iterrevs) # skip first rev.
262 262 # first step: get snapshots out of the way
263 263 for idx, r in iterrevs:
264 264 span = revlog.end(r) - startdata
265 265 snapshot = revlog.issnapshot(r)
266 266 if span <= targetsize and snapshot:
267 267 endrevidx = idx + 1
268 268 else:
269 269 chunk = _trimchunk(revlog, revs, startrevidx, endrevidx)
270 270 if chunk:
271 271 yield chunk
272 272 startrevidx = idx
273 273 startdata = revlog.start(r)
274 274 endrevidx = idx + 1
275 275 if not snapshot:
276 276 break
277 277
278 278 # for the others, we use binary slicing to quickly converge toward valid
279 279 # chunks (otherwise, we might end up looking for start/end of many
280 280 # revisions). This logic is not looking for the perfect slicing point, it
281 281 # focuses on quickly converging toward valid chunks.
282 282 nbitem = len(revs)
283 283 while (enddata - startdata) > targetsize:
284 284 endrevidx = nbitem
285 285 if nbitem - startrevidx <= 1:
286 286 break # protect against individual chunk larger than limit
287 287 localenddata = revlog.end(revs[endrevidx - 1])
288 288 span = localenddata - startdata
289 289 while span > targetsize:
290 290 if endrevidx - startrevidx <= 1:
291 291 break # protect against individual chunk larger than limit
292 292 endrevidx -= (endrevidx - startrevidx) // 2
293 293 localenddata = revlog.end(revs[endrevidx - 1])
294 294 span = localenddata - startdata
295 295 chunk = _trimchunk(revlog, revs, startrevidx, endrevidx)
296 296 if chunk:
297 297 yield chunk
298 298 startrevidx = endrevidx
299 299 startdata = revlog.start(revs[startrevidx])
300 300
301 301 chunk = _trimchunk(revlog, revs, startrevidx)
302 302 if chunk:
303 303 yield chunk
304 304
305 305
306 306 def _slicechunktodensity(revlog, revs, targetdensity=0.5, mingapsize=0):
307 307 """slice revs to reduce the amount of unrelated data to be read from disk.
308 308
309 309 ``revs`` is sliced into groups that should be read in one time.
310 310 Assume that revs are sorted.
311 311
312 312 The initial chunk is sliced until the overall density (payload/chunks-span
313 313 ratio) is above `targetdensity`. No gap smaller than `mingapsize` is
314 314 skipped.
315 315
316 316 >>> revlog = _testrevlog([
317 317 ... 5, #00 (5)
318 318 ... 10, #01 (5)
319 319 ... 12, #02 (2)
320 320 ... 12, #03 (empty)
321 321 ... 27, #04 (15)
322 322 ... 31, #05 (4)
323 323 ... 31, #06 (empty)
324 324 ... 42, #07 (11)
325 325 ... 47, #08 (5)
326 326 ... 47, #09 (empty)
327 327 ... 48, #10 (1)
328 328 ... 51, #11 (3)
329 329 ... 74, #12 (23)
330 330 ... 85, #13 (11)
331 331 ... 86, #14 (1)
332 332 ... 91, #15 (5)
333 333 ... ])
334 334
335 335 >>> list(_slicechunktodensity(revlog, list(range(16))))
336 336 [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]]
337 337 >>> list(_slicechunktodensity(revlog, [0, 15]))
338 338 [[0], [15]]
339 339 >>> list(_slicechunktodensity(revlog, [0, 11, 15]))
340 340 [[0], [11], [15]]
341 341 >>> list(_slicechunktodensity(revlog, [0, 11, 13, 15]))
342 342 [[0], [11, 13, 15]]
343 343 >>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14]))
344 344 [[1, 2], [5, 8, 10, 11], [14]]
345 345 >>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14],
346 346 ... mingapsize=20))
347 347 [[1, 2, 3, 5, 8, 10, 11], [14]]
348 348 >>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14],
349 349 ... targetdensity=0.95))
350 350 [[1, 2], [5], [8, 10, 11], [14]]
351 351 >>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14],
352 352 ... targetdensity=0.95, mingapsize=12))
353 353 [[1, 2], [5, 8, 10, 11], [14]]
354 354 """
355 355 start = revlog.start
356 356 length = revlog.length
357 357
358 358 if len(revs) <= 1:
359 359 yield revs
360 360 return
361 361
362 362 deltachainspan = segmentspan(revlog, revs)
363 363
364 364 if deltachainspan < mingapsize:
365 365 yield revs
366 366 return
367 367
368 368 readdata = deltachainspan
369 369 chainpayload = sum(length(r) for r in revs)
370 370
371 371 if deltachainspan:
372 372 density = chainpayload / float(deltachainspan)
373 373 else:
374 374 density = 1.0
375 375
376 376 if density >= targetdensity:
377 377 yield revs
378 378 return
379 379
380 380 # Store the gaps in a heap to have them sorted by decreasing size
381 381 gaps = []
382 382 prevend = None
383 383 for i, rev in enumerate(revs):
384 384 revstart = start(rev)
385 385 revlen = length(rev)
386 386
387 387 # Skip empty revisions to form larger holes
388 388 if revlen == 0:
389 389 continue
390 390
391 391 if prevend is not None:
392 392 gapsize = revstart - prevend
393 393 # only consider holes that are large enough
394 394 if gapsize > mingapsize:
395 395 gaps.append((gapsize, i))
396 396
397 397 prevend = revstart + revlen
398 398 # sort the gaps to pop them from largest to small
399 399 gaps.sort()
400 400
401 401 # Collect the indices of the largest holes until the density is acceptable
402 402 selected = []
403 403 while gaps and density < targetdensity:
404 404 gapsize, gapidx = gaps.pop()
405 405
406 406 selected.append(gapidx)
407 407
408 408 # the gap sizes are stored as negatives to be sorted decreasingly
409 409 # by the heap
410 410 readdata -= gapsize
411 411 if readdata > 0:
412 412 density = chainpayload / float(readdata)
413 413 else:
414 414 density = 1.0
415 415 selected.sort()
416 416
417 417 # Cut the revs at collected indices
418 418 previdx = 0
419 419 for idx in selected:
420 420
421 421 chunk = _trimchunk(revlog, revs, previdx, idx)
422 422 if chunk:
423 423 yield chunk
424 424
425 425 previdx = idx
426 426
427 427 chunk = _trimchunk(revlog, revs, previdx)
428 428 if chunk:
429 429 yield chunk
430 430
431 431
432 432 def _trimchunk(revlog, revs, startidx, endidx=None):
433 433 """returns revs[startidx:endidx] without empty trailing revs
434 434
435 435 Doctest Setup
436 436 >>> revlog = _testrevlog([
437 437 ... 5, #0
438 438 ... 10, #1
439 439 ... 12, #2
440 440 ... 12, #3 (empty)
441 441 ... 17, #4
442 442 ... 21, #5
443 443 ... 21, #6 (empty)
444 444 ... ])
445 445
446 446 Contiguous cases:
447 447 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 0)
448 448 [0, 1, 2, 3, 4, 5]
449 449 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 0, 5)
450 450 [0, 1, 2, 3, 4]
451 451 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 0, 4)
452 452 [0, 1, 2]
453 453 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 2, 4)
454 454 [2]
455 455 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 3)
456 456 [3, 4, 5]
457 457 >>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 3, 5)
458 458 [3, 4]
459 459
460 460 Discontiguous cases:
461 461 >>> _trimchunk(revlog, [1, 3, 5, 6], 0)
462 462 [1, 3, 5]
463 463 >>> _trimchunk(revlog, [1, 3, 5, 6], 0, 2)
464 464 [1]
465 465 >>> _trimchunk(revlog, [1, 3, 5, 6], 1, 3)
466 466 [3, 5]
467 467 >>> _trimchunk(revlog, [1, 3, 5, 6], 1)
468 468 [3, 5]
469 469 """
470 470 length = revlog.length
471 471
472 472 if endidx is None:
473 473 endidx = len(revs)
474 474
475 475 # If we have a non-emtpy delta candidate, there are nothing to trim
476 476 if revs[endidx - 1] < len(revlog):
477 477 # Trim empty revs at the end, except the very first revision of a chain
478 478 while (
479 479 endidx > 1 and endidx > startidx and length(revs[endidx - 1]) == 0
480 480 ):
481 481 endidx -= 1
482 482
483 483 return revs[startidx:endidx]
484 484
485 485
486 486 def segmentspan(revlog, revs):
487 487 """Get the byte span of a segment of revisions
488 488
489 489 revs is a sorted array of revision numbers
490 490
491 491 >>> revlog = _testrevlog([
492 492 ... 5, #0
493 493 ... 10, #1
494 494 ... 12, #2
495 495 ... 12, #3 (empty)
496 496 ... 17, #4
497 497 ... ])
498 498
499 499 >>> segmentspan(revlog, [0, 1, 2, 3, 4])
500 500 17
501 501 >>> segmentspan(revlog, [0, 4])
502 502 17
503 503 >>> segmentspan(revlog, [3, 4])
504 504 5
505 505 >>> segmentspan(revlog, [1, 2, 3,])
506 506 7
507 507 >>> segmentspan(revlog, [1, 3])
508 508 7
509 509 """
510 510 if not revs:
511 511 return 0
512 512 end = revlog.end(revs[-1])
513 513 return end - revlog.start(revs[0])
514 514
515 515
516 516 def _textfromdelta(fh, revlog, baserev, delta, p1, p2, flags, expectednode):
517 517 """build full text from a (base, delta) pair and other metadata"""
518 518 # special case deltas which replace entire base; no need to decode
519 519 # base revision. this neatly avoids censored bases, which throw when
520 520 # they're decoded.
521 521 hlen = struct.calcsize(b">lll")
522 522 if delta[:hlen] == mdiff.replacediffheader(
523 523 revlog.rawsize(baserev), len(delta) - hlen
524 524 ):
525 525 fulltext = delta[hlen:]
526 526 else:
527 527 # deltabase is rawtext before changed by flag processors, which is
528 528 # equivalent to non-raw text
529 529 basetext = revlog.revision(baserev, _df=fh, raw=False)
530 530 fulltext = mdiff.patch(basetext, delta)
531 531
532 532 try:
533 533 validatehash = flagutil.processflagsraw(revlog, fulltext, flags)
534 534 if validatehash:
535 535 revlog.checkhash(fulltext, expectednode, p1=p1, p2=p2)
536 536 if flags & REVIDX_ISCENSORED:
537 537 raise error.StorageError(
538 538 _(b'node %s is not censored') % expectednode
539 539 )
540 540 except error.CensoredNodeError:
541 541 # must pass the censored index flag to add censored revisions
542 542 if not flags & REVIDX_ISCENSORED:
543 543 raise
544 544 return fulltext
545 545
546 546
547 547 @attr.s(slots=True, frozen=True)
548 548 class _deltainfo(object):
549 549 distance = attr.ib()
550 550 deltalen = attr.ib()
551 551 data = attr.ib()
552 552 base = attr.ib()
553 553 chainbase = attr.ib()
554 554 chainlen = attr.ib()
555 555 compresseddeltalen = attr.ib()
556 556 snapshotdepth = attr.ib()
557 557
558 558
559 559 def drop_u_compression(delta):
560 560 """turn into a "u" (no-compression) into no-compression without header
561 561
562 562 This is useful for revlog format that has better compression method.
563 563 """
564 564 assert delta.data[0] == b'u', delta.data[0]
565 565 return _deltainfo(
566 566 delta.distance,
567 567 delta.deltalen - 1,
568 568 (b'', delta.data[1]),
569 569 delta.base,
570 570 delta.chainbase,
571 571 delta.chainlen,
572 572 delta.compresseddeltalen,
573 573 delta.snapshotdepth,
574 574 )
575 575
576 576
577 577 def isgooddeltainfo(revlog, deltainfo, revinfo):
578 578 """Returns True if the given delta is good. Good means that it is within
579 579 the disk span, disk size, and chain length bounds that we know to be
580 580 performant."""
581 581 if deltainfo is None:
582 582 return False
583 583
584 584 # - 'deltainfo.distance' is the distance from the base revision --
585 585 # bounding it limits the amount of I/O we need to do.
586 586 # - 'deltainfo.compresseddeltalen' is the sum of the total size of
587 587 # deltas we need to apply -- bounding it limits the amount of CPU
588 588 # we consume.
589 589
590 590 textlen = revinfo.textlen
591 591 defaultmax = textlen * 4
592 592 maxdist = revlog._maxdeltachainspan
593 593 if not maxdist:
594 594 maxdist = deltainfo.distance # ensure the conditional pass
595 595 maxdist = max(maxdist, defaultmax)
596 596
597 597 # Bad delta from read span:
598 598 #
599 599 # If the span of data read is larger than the maximum allowed.
600 600 #
601 601 # In the sparse-revlog case, we rely on the associated "sparse reading"
602 602 # to avoid issue related to the span of data. In theory, it would be
603 603 # possible to build pathological revlog where delta pattern would lead
604 604 # to too many reads. However, they do not happen in practice at all. So
605 605 # we skip the span check entirely.
606 606 if not revlog._sparserevlog and maxdist < deltainfo.distance:
607 607 return False
608 608
609 609 # Bad delta from new delta size:
610 610 #
611 611 # If the delta size is larger than the target text, storing the
612 612 # delta will be inefficient.
613 613 if textlen < deltainfo.deltalen:
614 614 return False
615 615
616 616 # Bad delta from cumulated payload size:
617 617 #
618 618 # If the sum of delta get larger than K * target text length.
619 619 if textlen * LIMIT_DELTA2TEXT < deltainfo.compresseddeltalen:
620 620 return False
621 621
622 622 # Bad delta from chain length:
623 623 #
624 624 # If the number of delta in the chain gets too high.
625 625 if revlog._maxchainlen and revlog._maxchainlen < deltainfo.chainlen:
626 626 return False
627 627
628 628 # bad delta from intermediate snapshot size limit
629 629 #
630 630 # If an intermediate snapshot size is higher than the limit. The
631 631 # limit exist to prevent endless chain of intermediate delta to be
632 632 # created.
633 633 if (
634 634 deltainfo.snapshotdepth is not None
635 635 and (textlen >> deltainfo.snapshotdepth) < deltainfo.deltalen
636 636 ):
637 637 return False
638 638
639 639 # bad delta if new intermediate snapshot is larger than the previous
640 640 # snapshot
641 641 if (
642 642 deltainfo.snapshotdepth
643 643 and revlog.length(deltainfo.base) < deltainfo.deltalen
644 644 ):
645 645 return False
646 646
647 647 return True
648 648
649 649
650 650 # If a revision's full text is that much bigger than a base candidate full
651 651 # text's, it is very unlikely that it will produce a valid delta. We no longer
652 652 # consider these candidates.
653 653 LIMIT_BASE2TEXT = 500
654 654
655 655
656 656 def _candidategroups(revlog, textlen, p1, p2, cachedelta):
657 657 """Provides group of revision to be tested as delta base
658 658
659 659 This top level function focus on emitting groups with unique and worthwhile
660 660 content. See _raw_candidate_groups for details about the group order.
661 661 """
662 662 # should we try to build a delta?
663 663 if not (len(revlog) and revlog._storedeltachains):
664 664 yield None
665 665 return
666 666
667 667 deltalength = revlog.length
668 668 deltaparent = revlog.deltaparent
669 669 sparse = revlog._sparserevlog
670 670 good = None
671 671
672 672 deltas_limit = textlen * LIMIT_DELTA2TEXT
673 673
674 674 tested = {nullrev}
675 675 candidates = _refinedgroups(revlog, p1, p2, cachedelta)
676 676 while True:
677 677 temptative = candidates.send(good)
678 678 if temptative is None:
679 679 break
680 680 group = []
681 681 for rev in temptative:
682 682 # skip over empty delta (no need to include them in a chain)
683 683 while revlog._generaldelta and not (
684 684 rev == nullrev or rev in tested or deltalength(rev)
685 685 ):
686 686 tested.add(rev)
687 687 rev = deltaparent(rev)
688 688 # no need to try a delta against nullrev, this will be done as a
689 689 # last resort.
690 690 if rev == nullrev:
691 691 continue
692 692 # filter out revision we tested already
693 693 if rev in tested:
694 694 continue
695 695 tested.add(rev)
696 696 # filter out delta base that will never produce good delta
697 697 if deltas_limit < revlog.length(rev):
698 698 continue
699 699 if sparse and revlog.rawsize(rev) < (textlen // LIMIT_BASE2TEXT):
700 700 continue
701 701 # no delta for rawtext-changing revs (see "candelta" for why)
702 702 if revlog.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS:
703 703 continue
704 704 # If we reach here, we are about to build and test a delta.
705 705 # The delta building process will compute the chaininfo in all
706 706 # case, since that computation is cached, it is fine to access it
707 707 # here too.
708 708 chainlen, chainsize = revlog._chaininfo(rev)
709 709 # if chain will be too long, skip base
710 710 if revlog._maxchainlen and chainlen >= revlog._maxchainlen:
711 711 continue
712 712 # if chain already have too much data, skip base
713 713 if deltas_limit < chainsize:
714 714 continue
715 715 if sparse and revlog.upperboundcomp is not None:
716 716 maxcomp = revlog.upperboundcomp
717 717 basenotsnap = (p1, p2, nullrev)
718 718 if rev not in basenotsnap and revlog.issnapshot(rev):
719 719 snapshotdepth = revlog.snapshotdepth(rev)
720 720 # If text is significantly larger than the base, we can
721 721 # expect the resulting delta to be proportional to the size
722 722 # difference
723 723 revsize = revlog.rawsize(rev)
724 724 rawsizedistance = max(textlen - revsize, 0)
725 725 # use an estimate of the compression upper bound.
726 726 lowestrealisticdeltalen = rawsizedistance // maxcomp
727 727
728 728 # check the absolute constraint on the delta size
729 729 snapshotlimit = textlen >> snapshotdepth
730 730 if snapshotlimit < lowestrealisticdeltalen:
731 731 # delta lower bound is larger than accepted upper bound
732 732 continue
733 733
734 734 # check the relative constraint on the delta size
735 735 revlength = revlog.length(rev)
736 736 if revlength < lowestrealisticdeltalen:
737 737 # delta probable lower bound is larger than target base
738 738 continue
739 739
740 740 group.append(rev)
741 741 if group:
742 742 # XXX: in the sparse revlog case, group can become large,
743 743 # impacting performances. Some bounding or slicing mecanism
744 744 # would help to reduce this impact.
745 745 good = yield tuple(group)
746 746 yield None
747 747
748 748
749 749 def _findsnapshots(revlog, cache, start_rev):
750 750 """find snapshot from start_rev to tip"""
751 751 if util.safehasattr(revlog.index, b'findsnapshots'):
752 752 revlog.index.findsnapshots(cache, start_rev)
753 753 else:
754 754 deltaparent = revlog.deltaparent
755 755 issnapshot = revlog.issnapshot
756 756 for rev in revlog.revs(start_rev):
757 757 if issnapshot(rev):
758 758 cache[deltaparent(rev)].append(rev)
759 759
760 760
761 761 def _refinedgroups(revlog, p1, p2, cachedelta):
762 762 good = None
763 763 # First we try to reuse a the delta contained in the bundle.
764 764 # (or from the source revlog)
765 765 #
766 766 # This logic only applies to general delta repositories and can be disabled
767 767 # through configuration. Disabling reuse source delta is useful when
768 768 # we want to make sure we recomputed "optimal" deltas.
769 769 if cachedelta and revlog._generaldelta and revlog._lazydeltabase:
770 770 # Assume what we received from the server is a good choice
771 771 # build delta will reuse the cache
772 772 good = yield (cachedelta[0],)
773 773 if good is not None:
774 774 yield None
775 775 return
776 776 snapshots = collections.defaultdict(list)
777 777 for candidates in _rawgroups(revlog, p1, p2, cachedelta, snapshots):
778 778 good = yield candidates
779 779 if good is not None:
780 780 break
781 781
782 782 # If sparse revlog is enabled, we can try to refine the available deltas
783 783 if not revlog._sparserevlog:
784 784 yield None
785 785 return
786 786
787 787 # if we have a refinable value, try to refine it
788 788 if good is not None and good not in (p1, p2) and revlog.issnapshot(good):
789 789 # refine snapshot down
790 790 previous = None
791 791 while previous != good:
792 792 previous = good
793 793 base = revlog.deltaparent(good)
794 794 if base == nullrev:
795 795 break
796 796 good = yield (base,)
797 797 # refine snapshot up
798 798 if not snapshots:
799 799 _findsnapshots(revlog, snapshots, good + 1)
800 800 previous = None
801 801 while good != previous:
802 802 previous = good
803 803 children = tuple(sorted(c for c in snapshots[good]))
804 804 good = yield children
805 805
806 806 # we have found nothing
807 807 yield None
808 808
809 809
810 810 def _rawgroups(revlog, p1, p2, cachedelta, snapshots=None):
811 811 """Provides group of revision to be tested as delta base
812 812
813 813 This lower level function focus on emitting delta theorically interresting
814 814 without looking it any practical details.
815 815
816 816 The group order aims at providing fast or small candidates first.
817 817 """
818 818 gdelta = revlog._generaldelta
819 819 # gate sparse behind general-delta because of issue6056
820 820 sparse = gdelta and revlog._sparserevlog
821 821 curr = len(revlog)
822 822 prev = curr - 1
823 823 deltachain = lambda rev: revlog._deltachain(rev)[0]
824 824
825 825 if gdelta:
826 826 # exclude already lazy tested base if any
827 827 parents = [p for p in (p1, p2) if p != nullrev]
828 828
829 829 if not revlog._deltabothparents and len(parents) == 2:
830 830 parents.sort()
831 831 # To minimize the chance of having to build a fulltext,
832 832 # pick first whichever parent is closest to us (max rev)
833 833 yield (parents[1],)
834 834 # then the other one (min rev) if the first did not fit
835 835 yield (parents[0],)
836 836 elif len(parents) > 0:
837 837 # Test all parents (1 or 2), and keep the best candidate
838 838 yield parents
839 839
840 840 if sparse and parents:
841 841 if snapshots is None:
842 842 # map: base-rev: snapshot-rev
843 843 snapshots = collections.defaultdict(list)
844 844 # See if we can use an existing snapshot in the parent chains to use as
845 845 # a base for a new intermediate-snapshot
846 846 #
847 847 # search for snapshot in parents delta chain
848 848 # map: snapshot-level: snapshot-rev
849 849 parents_snaps = collections.defaultdict(set)
850 850 candidate_chains = [deltachain(p) for p in parents]
851 851 for chain in candidate_chains:
852 852 for idx, s in enumerate(chain):
853 853 if not revlog.issnapshot(s):
854 854 break
855 855 parents_snaps[idx].add(s)
856 856 snapfloor = min(parents_snaps[0]) + 1
857 857 _findsnapshots(revlog, snapshots, snapfloor)
858 858 # search for the highest "unrelated" revision
859 859 #
860 860 # Adding snapshots used by "unrelated" revision increase the odd we
861 861 # reuse an independant, yet better snapshot chain.
862 862 #
863 863 # XXX instead of building a set of revisions, we could lazily enumerate
864 864 # over the chains. That would be more efficient, however we stick to
865 865 # simple code for now.
866 866 all_revs = set()
867 867 for chain in candidate_chains:
868 868 all_revs.update(chain)
869 869 other = None
870 870 for r in revlog.revs(prev, snapfloor):
871 871 if r not in all_revs:
872 872 other = r
873 873 break
874 874 if other is not None:
875 875 # To avoid unfair competition, we won't use unrelated intermediate
876 876 # snapshot that are deeper than the ones from the parent delta
877 877 # chain.
878 878 max_depth = max(parents_snaps.keys())
879 879 chain = deltachain(other)
880 880 for idx, s in enumerate(chain):
881 881 if s < snapfloor:
882 882 continue
883 883 if max_depth < idx:
884 884 break
885 885 if not revlog.issnapshot(s):
886 886 break
887 887 parents_snaps[idx].add(s)
888 888 # Test them as possible intermediate snapshot base
889 889 # We test them from highest to lowest level. High level one are more
890 890 # likely to result in small delta
891 891 floor = None
892 892 for idx, snaps in sorted(parents_snaps.items(), reverse=True):
893 893 siblings = set()
894 894 for s in snaps:
895 895 siblings.update(snapshots[s])
896 896 # Before considering making a new intermediate snapshot, we check
897 897 # if an existing snapshot, children of base we consider, would be
898 898 # suitable.
899 899 #
900 900 # It give a change to reuse a delta chain "unrelated" to the
901 901 # current revision instead of starting our own. Without such
902 902 # re-use, topological branches would keep reopening new chains.
903 903 # Creating more and more snapshot as the repository grow.
904 904
905 905 if floor is not None:
906 906 # We only do this for siblings created after the one in our
907 907 # parent's delta chain. Those created before has less chances
908 908 # to be valid base since our ancestors had to create a new
909 909 # snapshot.
910 910 siblings = [r for r in siblings if floor < r]
911 911 yield tuple(sorted(siblings))
912 912 # then test the base from our parent's delta chain.
913 913 yield tuple(sorted(snaps))
914 914 floor = min(snaps)
915 915 # No suitable base found in the parent chain, search if any full
916 916 # snapshots emitted since parent's base would be a suitable base for an
917 917 # intermediate snapshot.
918 918 #
919 919 # It give a chance to reuse a delta chain unrelated to the current
920 920 # revisions instead of starting our own. Without such re-use,
921 921 # topological branches would keep reopening new full chains. Creating
922 922 # more and more snapshot as the repository grow.
923 923 yield tuple(snapshots[nullrev])
924 924
925 925 if not sparse:
926 926 # other approach failed try against prev to hopefully save us a
927 927 # fulltext.
928 928 yield (prev,)
929 929
930 930
931 931 class deltacomputer(object):
932 932 def __init__(self, revlog):
933 933 self.revlog = revlog
934 934
935 935 def buildtext(self, revinfo, fh):
936 936 """Builds a fulltext version of a revision
937 937
938 938 revinfo: revisioninfo instance that contains all needed info
939 939 fh: file handle to either the .i or the .d revlog file,
940 940 depending on whether it is inlined or not
941 941 """
942 942 btext = revinfo.btext
943 943 if btext[0] is not None:
944 944 return btext[0]
945 945
946 946 revlog = self.revlog
947 947 cachedelta = revinfo.cachedelta
948 948 baserev = cachedelta[0]
949 949 delta = cachedelta[1]
950 950
951 951 fulltext = btext[0] = _textfromdelta(
952 952 fh,
953 953 revlog,
954 954 baserev,
955 955 delta,
956 956 revinfo.p1,
957 957 revinfo.p2,
958 958 revinfo.flags,
959 959 revinfo.node,
960 960 )
961 961 return fulltext
962 962
963 963 def _builddeltadiff(self, base, revinfo, fh):
964 964 revlog = self.revlog
965 965 t = self.buildtext(revinfo, fh)
966 966 if revlog.iscensored(base):
967 967 # deltas based on a censored revision must replace the
968 968 # full content in one patch, so delta works everywhere
969 969 header = mdiff.replacediffheader(revlog.rawsize(base), len(t))
970 970 delta = header + t
971 971 else:
972 972 ptext = revlog.rawdata(base, _df=fh)
973 973 delta = mdiff.textdiff(ptext, t)
974 974
975 975 return delta
976 976
977 977 def _builddeltainfo(self, revinfo, base, fh):
978 978 # can we use the cached delta?
979 979 revlog = self.revlog
980 980 chainbase = revlog.chainbase(base)
981 981 if revlog._generaldelta:
982 982 deltabase = base
983 983 else:
984 984 deltabase = chainbase
985 985 snapshotdepth = None
986 986 if revlog._sparserevlog and deltabase == nullrev:
987 987 snapshotdepth = 0
988 988 elif revlog._sparserevlog and revlog.issnapshot(deltabase):
989 989 # A delta chain should always be one full snapshot,
990 990 # zero or more semi-snapshots, and zero or more deltas
991 991 p1, p2 = revlog.rev(revinfo.p1), revlog.rev(revinfo.p2)
992 992 if deltabase not in (p1, p2) and revlog.issnapshot(deltabase):
993 993 snapshotdepth = len(revlog._deltachain(deltabase)[0])
994 994 delta = None
995 995 if revinfo.cachedelta:
996 996 cachebase, cachediff = revinfo.cachedelta
997 997 # check if the diff still apply
998 998 currentbase = cachebase
999 999 while (
1000 1000 currentbase != nullrev
1001 1001 and currentbase != base
1002 1002 and self.revlog.length(currentbase) == 0
1003 1003 ):
1004 1004 currentbase = self.revlog.deltaparent(currentbase)
1005 1005 if self.revlog._lazydelta and currentbase == base:
1006 1006 delta = revinfo.cachedelta[1]
1007 1007 if delta is None:
1008 1008 delta = self._builddeltadiff(base, revinfo, fh)
1009 1009 # snapshotdept need to be neither None nor 0 level snapshot
1010 1010 if revlog.upperboundcomp is not None and snapshotdepth:
1011 1011 lowestrealisticdeltalen = len(delta) // revlog.upperboundcomp
1012 1012 snapshotlimit = revinfo.textlen >> snapshotdepth
1013 1013 if snapshotlimit < lowestrealisticdeltalen:
1014 1014 return None
1015 1015 if revlog.length(base) < lowestrealisticdeltalen:
1016 1016 return None
1017 1017 header, data = revlog.compress(delta)
1018 1018 deltalen = len(header) + len(data)
1019 1019 offset = revlog.end(len(revlog) - 1)
1020 1020 dist = deltalen + offset - revlog.start(chainbase)
1021 1021 chainlen, compresseddeltalen = revlog._chaininfo(base)
1022 1022 chainlen += 1
1023 1023 compresseddeltalen += deltalen
1024 1024
1025 1025 return _deltainfo(
1026 1026 dist,
1027 1027 deltalen,
1028 1028 (header, data),
1029 1029 deltabase,
1030 1030 chainbase,
1031 1031 chainlen,
1032 1032 compresseddeltalen,
1033 1033 snapshotdepth,
1034 1034 )
1035 1035
1036 def _fullsnapshotinfo(self, fh, revinfo):
1037 curr = len(self.revlog)
1036 def _fullsnapshotinfo(self, fh, revinfo, curr):
1038 1037 rawtext = self.buildtext(revinfo, fh)
1039 1038 data = self.revlog.compress(rawtext)
1040 1039 compresseddeltalen = deltalen = dist = len(data[1]) + len(data[0])
1041 1040 deltabase = chainbase = curr
1042 1041 snapshotdepth = 0
1043 1042 chainlen = 1
1044 1043
1045 1044 return _deltainfo(
1046 1045 dist,
1047 1046 deltalen,
1048 1047 data,
1049 1048 deltabase,
1050 1049 chainbase,
1051 1050 chainlen,
1052 1051 compresseddeltalen,
1053 1052 snapshotdepth,
1054 1053 )
1055 1054
1056 def finddeltainfo(self, revinfo, fh, excluded_bases=None):
1055 def finddeltainfo(self, revinfo, fh, excluded_bases=None, target_rev=None):
1057 1056 """Find an acceptable delta against a candidate revision
1058 1057
1059 1058 revinfo: information about the revision (instance of _revisioninfo)
1060 1059 fh: file handle to either the .i or the .d revlog file,
1061 1060 depending on whether it is inlined or not
1062 1061
1063 1062 Returns the first acceptable candidate revision, as ordered by
1064 1063 _candidategroups
1065 1064
1066 1065 If no suitable deltabase is found, we return delta info for a full
1067 1066 snapshot.
1068 1067
1069 1068 `excluded_bases` is an optional set of revision that cannot be used as
1070 1069 a delta base. Use this to recompute delta suitable in censor or strip
1071 1070 context.
1072 1071 """
1072 if target_rev is None:
1073 curr = len(self.revlog)
1074
1073 1075 if not revinfo.textlen:
1074 return self._fullsnapshotinfo(fh, revinfo)
1076 return self._fullsnapshotinfo(fh, revinfo, target_rev)
1075 1077
1076 1078 if excluded_bases is None:
1077 1079 excluded_bases = set()
1078 1080
1079 1081 # no delta for flag processor revision (see "candelta" for why)
1080 1082 # not calling candelta since only one revision needs test, also to
1081 1083 # avoid overhead fetching flags again.
1082 1084 if revinfo.flags & REVIDX_RAWTEXT_CHANGING_FLAGS:
1083 return self._fullsnapshotinfo(fh, revinfo)
1085 return self._fullsnapshotinfo(fh, revinfo, target_rev)
1084 1086
1085 1087 cachedelta = revinfo.cachedelta
1086 1088 p1 = revinfo.p1
1087 1089 p2 = revinfo.p2
1088 1090 revlog = self.revlog
1089 1091
1090 1092 deltainfo = None
1091 1093 p1r, p2r = revlog.rev(p1), revlog.rev(p2)
1092 1094 groups = _candidategroups(
1093 1095 self.revlog, revinfo.textlen, p1r, p2r, cachedelta
1094 1096 )
1095 1097 candidaterevs = next(groups)
1096 1098 while candidaterevs is not None:
1097 1099 nominateddeltas = []
1098 1100 if deltainfo is not None:
1099 1101 # if we already found a good delta,
1100 1102 # challenge it against refined candidates
1101 1103 nominateddeltas.append(deltainfo)
1102 1104 for candidaterev in candidaterevs:
1103 1105 if candidaterev in excluded_bases:
1104 1106 continue
1107 if candidaterev >= target_rev:
1108 continue
1105 1109 candidatedelta = self._builddeltainfo(revinfo, candidaterev, fh)
1106 1110 if candidatedelta is not None:
1107 1111 if isgooddeltainfo(self.revlog, candidatedelta, revinfo):
1108 1112 nominateddeltas.append(candidatedelta)
1109 1113 if nominateddeltas:
1110 1114 deltainfo = min(nominateddeltas, key=lambda x: x.deltalen)
1111 1115 if deltainfo is not None:
1112 1116 candidaterevs = groups.send(deltainfo.base)
1113 1117 else:
1114 1118 candidaterevs = next(groups)
1115 1119
1116 1120 if deltainfo is None:
1117 deltainfo = self._fullsnapshotinfo(fh, revinfo)
1121 deltainfo = self._fullsnapshotinfo(fh, revinfo, target_rev)
1118 1122 return deltainfo
1119 1123
1120 1124
1121 1125 def delta_compression(default_compression_header, deltainfo):
1122 1126 """return (COMPRESSION_MODE, deltainfo)
1123 1127
1124 1128 used by revlog v2+ format to dispatch between PLAIN and DEFAULT
1125 1129 compression.
1126 1130 """
1127 1131 h, d = deltainfo.data
1128 1132 compression_mode = COMP_MODE_INLINE
1129 1133 if not h and not d:
1130 1134 # not data to store at all... declare them uncompressed
1131 1135 compression_mode = COMP_MODE_PLAIN
1132 1136 elif not h:
1133 1137 t = d[0:1]
1134 1138 if t == b'\0':
1135 1139 compression_mode = COMP_MODE_PLAIN
1136 1140 elif t == default_compression_header:
1137 1141 compression_mode = COMP_MODE_DEFAULT
1138 1142 elif h == b'u':
1139 1143 # we have a more efficient way to declare uncompressed
1140 1144 h = b''
1141 1145 compression_mode = COMP_MODE_PLAIN
1142 1146 deltainfo = drop_u_compression(deltainfo)
1143 1147 return compression_mode, deltainfo
General Comments 0
You need to be logged in to leave comments. Login now