Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions bsdiff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ type DiffContext struct {

Stats *DiffStats

db bytes.Buffer
// addBuf is reused across matches to hold the byte-difference payload of
// each Control.Add. Sized to match the current match length; capacity
// grows monotonically.
addBuf []byte

obuf bytes.Buffer
nbuf bytes.Buffer
Expand Down Expand Up @@ -109,14 +112,17 @@ func (ctx *DiffContext) writeMessages(obuf []byte, nbuf []byte, matches chan Mat
}
}

ctx.db.Reset()
ctx.db.Grow(match.addLength)

for i := 0; i < match.addLength; i++ {
ctx.db.WriteByte(nbuf[match.addNewStart+i] - obuf[match.addOldStart+i])
if cap(ctx.addBuf) < match.addLength {
ctx.addBuf = make([]byte, match.addLength)
} else {
ctx.addBuf = ctx.addBuf[:match.addLength]
}
subtractInto(ctx.addBuf,
nbuf[match.addNewStart:match.addNewStart+match.addLength],
obuf[match.addOldStart:match.addOldStart+match.addLength],
)

bsdc.Add = ctx.db.Bytes()
bsdc.Add = ctx.addBuf
bsdc.Copy = nbuf[match.copyStart():match.copyEnd]

if ctx.Stats != nil && ctx.Stats.BiggestAdd < int64(len(bsdc.Add)) {
Expand Down
32 changes: 32 additions & 0 deletions bsdiff/subtract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package bsdiff

// subtractInto writes dst[i] = a[i] - b[i] (mod 256) for i in [0, len(dst)).
// a and b must each have at least len(dst) bytes. The hot inner loop is
// unrolled 8-wide so the amd64 backend can keep the pipeline full and
// elide the per-iteration bounds checks — replacing the original
// bytes.Buffer.WriteByte loop in writeMessages.
func subtractInto(dst, a, b []byte) {
n := len(dst)
if n == 0 {
return
}
// Bounds-check hints: tell the compiler that a[n-1] and b[n-1] are
// in range, so it can drop the bounds check on each indexed access
// inside the loop.
_ = a[n-1]
_ = b[n-1]
i := 0
for ; i+8 <= n; i += 8 {
dst[i] = a[i] - b[i]
dst[i+1] = a[i+1] - b[i+1]
dst[i+2] = a[i+2] - b[i+2]
dst[i+3] = a[i+3] - b[i+3]
dst[i+4] = a[i+4] - b[i+4]
dst[i+5] = a[i+5] - b[i+5]
dst[i+6] = a[i+6] - b[i+6]
dst[i+7] = a[i+7] - b[i+7]
}
for ; i < n; i++ {
dst[i] = a[i] - b[i]
}
}
66 changes: 43 additions & 23 deletions pwr/rediff/rediff.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rediff

import (
"bytes"
"fmt"
"io"
"strings"
Expand All @@ -11,6 +12,7 @@ import (
"github.com/itchio/lake"
"github.com/itchio/lake/tlc"
"github.com/itchio/savior"
"github.com/itchio/savior/seeksource"
"github.com/itchio/wharf/bsdiff"
"github.com/itchio/wharf/pwr"
"github.com/itchio/wharf/wire"
Expand Down Expand Up @@ -54,6 +56,13 @@ type context struct {
targetContainer *tlc.Container
sourceContainer *tlc.Container
diffMappings DiffMappings

// decompressedBody holds the entire decompressed patch body (everything
// after the magic + PatchHeader). analyzePatch reads it once via the
// real decompressor and caches it here; Optimize then reads from this
// buffer directly, skipping the redundant second brotli decompression.
// Cleared at the end of Optimize to release the memory.
decompressedBody []byte
}

type Context interface {
Expand Down Expand Up @@ -151,11 +160,26 @@ func (cx *context) analyzePatch() error {
return err
}

rctx, err = pwr.DecompressWire(rctx, ph.Compression)
decompRctx, err := pwr.DecompressWire(rctx, ph.Compression)
if err != nil {
return errors.WithStack(err)
}

// Drain the entire decompressed body into a buffer here so Optimize
// can re-read it without paying for a second brotli decode. Memory
// cost: full decompressed body (typically 1.5–2× compressed patch
// size). Released at the end of Optimize.
var decompBuf bytes.Buffer
if _, err := io.Copy(&decompBuf, decompRctx.GetSource()); err != nil {
return errors.WithStack(err)
}
cx.decompressedBody = decompBuf.Bytes()

rctx = wire.NewReadContext(seeksource.FromBytes(cx.decompressedBody))
if _, err := rctx.GetSource().Resume(nil); err != nil {
return errors.WithStack(err)
}

targetContainer := &tlc.Container{}
err = rctx.ReadMessage(targetContainer)
if err != nil {
Expand Down Expand Up @@ -305,26 +329,19 @@ func (cx *context) Optimize(params OptimizeParams) error {
return err
}

_, err = cx.params.PatchReader.Resume(nil)
if err != nil {
return err
}

rctx := wire.NewReadContext(cx.params.PatchReader)
wctx := wire.NewWriteContext(params.PatchWriter)

err = wctx.WriteMagic(pwr.PatchMagic)
if err != nil {
return errors.WithStack(err)
// Read from the analyzePatch-populated decompressedBody cache instead
// of re-decompressing the patch. Saves a full brotli decode pass on
// the input side.
if cx.decompressedBody == nil {
return errors.Errorf("rediff: Optimize called without prior analyzePatch (decompressed body cache missing)")
}

err = rctx.ExpectMagic(pwr.PatchMagic)
if err != nil {
rctx := wire.NewReadContext(seeksource.FromBytes(cx.decompressedBody))
if _, err := rctx.GetSource().Resume(nil); err != nil {
return errors.WithStack(err)
}
wctx := wire.NewWriteContext(params.PatchWriter)

ph := &pwr.PatchHeader{}
err = rctx.ReadMessage(ph)
err = wctx.WriteMagic(pwr.PatchMagic)
if err != nil {
return errors.WithStack(err)
}
Expand All @@ -342,11 +359,6 @@ func (cx *context) Optimize(params OptimizeParams) error {
return errors.WithStack(err)
}

rctx, err = pwr.DecompressWire(rctx, ph.Compression)
if err != nil {
return errors.WithStack(err)
}

wctx, err = pwr.CompressWire(wctx, wph.Compression)
if err != nil {
return errors.WithStack(err)
Expand Down Expand Up @@ -519,6 +531,9 @@ func (cx *context) Optimize(params OptimizeParams) error {
return errors.WithStack(err)
}

// Release the decompressed body cache now that we're done with it.
cx.decompressedBody = nil

return nil
}

Expand All @@ -538,9 +553,14 @@ func (cx *context) GetDiffMappings() DiffMappings {
return cx.diffMappings
}

// defaultRediffCompressionSettings sets the brotli quality used when the
// caller doesn't pass Params.Compression. q=7 trades a small (<1%) size
// growth for materially faster brotli encode on already-bsdiff'd Control
// streams. q=6 was tried first and produced +1.75% size on a real-world
// 275MB patch, violating the ±1% size gate.
func defaultRediffCompressionSettings() *pwr.CompressionSettings {
return &pwr.CompressionSettings{
Algorithm: pwr.CompressionAlgorithm_BROTLI,
Quality: 9,
Quality: 7,
}
}
Loading