go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/copyonwrite/cow.go (about)

     1  // Copyright 2021 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package copyonwrite providers helpers for modifying slices in Copy-on-Write way.
    16  package copyonwrite
    17  
    18  import (
    19  	"fmt"
    20  	"sort"
    21  )
    22  
    23  // Slice abstracts out copy-on-write slices of pointers to objects.
    24  type Slice interface {
    25  	Len() int
    26  	At(index int) any
    27  	Append(v any) Slice
    28  	// CloneShallow returns new slice of the given capacity with elements copied
    29  	// from [:length] of this slice.
    30  	CloneShallow(length, capacity int) Slice
    31  }
    32  
    33  // SortedSlice is a sorted Slice.
    34  type SortedSlice interface {
    35  	Slice
    36  	sort.Interface
    37  	// LessElements returns true if element `a` must be before `b`.
    38  	//
    39  	// Unlike sort.Interface's Less, this compares arbitrary elements.
    40  	//
    41  	// Why not use sort.Interface's Less?
    42  	// Because during merge phase of (newly created, updated) slices, it's
    43  	// necessary to compare elements from different slices. OK, OK, it's possible
    44  	// to move both elements to one slice and re-use Less. For example, a temp
    45  	// slice of 2 elements can be always allocated for this very purpose, or one
    46  	// can smartly re-use resulting slice for this. However, either way it's
    47  	// slower and more complicated than requiring boilerplate in each SortedSlice
    48  	// type, which can be re-used in Less() implementation.
    49  	LessElements(a, b any) bool
    50  }
    51  
    52  // Deletion is special return value for Modifier to imply deletion.
    53  var Deletion = deletion{}
    54  
    55  type deletion struct{}
    56  
    57  // Modifier takes a pointer to an existing COW object and returns:
    58  //   - special DeleteMe, if this object should be deleted from Slice;
    59  //   - the same object, if there are no modifications;
    60  //   - new object, if there were modifications. If working with SortedSlice,
    61  //     the new object must have the same sorting key.
    62  type Modifier func(any) any
    63  
    64  // Update modifies existing elements and adds new ones to Slice.
    65  //
    66  // If given Slice implements SortedSlice, assumes and preserves its sorted
    67  // order.
    68  //
    69  // COWModifier, if not nil, is called on each existing object to modify or
    70  // possible delete it.
    71  //
    72  // Sorts toAdd slice if using SortedSlice.
    73  //
    74  // Returns (new slice, true) if it was modified.
    75  // Returns (old slice, false) otherwise.
    76  func Update(in Slice, modifier Modifier, toAdd Slice) (Slice, bool) {
    77  	inLen := 0
    78  	if in != nil {
    79  		inLen = in.Len()
    80  	}
    81  	if modifier == nil {
    82  		modifier = noopModifier
    83  	}
    84  	cLen := 0
    85  	if toAdd != nil {
    86  		cLen = toAdd.Len()
    87  	}
    88  	_, isSorted := in.(SortedSlice)
    89  	if isSorted && cLen > 0 {
    90  		sortable, ok := toAdd.(SortedSlice)
    91  		if !ok {
    92  			panic(fmt.Errorf("Different types for in and toAdd slices: %T vs %T", in, toAdd))
    93  		}
    94  		sort.Sort(sortable)
    95  	}
    96  	switch {
    97  	case inLen == 0 && cLen == 0:
    98  		return in, false
    99  	case inLen == 0:
   100  		return toAdd, true
   101  	case cLen == 0:
   102  		return modifyRemaining(in, 0, modifier, nil)
   103  	case !isSorted:
   104  		return modifyRemaining(in, 0, modifier, toAdd)
   105  	}
   106  
   107  	// Merge two sorted sequences into `out` while modifying `in` elements at the
   108  	// same time.
   109  	less := in.(SortedSlice).LessElements
   110  	out := in.CloneShallow(0, inLen+cLen)
   111  	i, c := 0, 0
   112  	for {
   113  		if c == cLen {
   114  			return modifyRemaining(in, i, modifier, out)
   115  		}
   116  		if i == inLen {
   117  			for ; c < cLen; c++ {
   118  				out = out.Append(toAdd.At(c))
   119  			}
   120  			return out, true
   121  		}
   122  		iOld := in.At(i)
   123  		cNew := toAdd.At(c)
   124  		if less(cNew, iOld) {
   125  			c++
   126  			out = out.Append(cNew)
   127  			continue
   128  		}
   129  		i++
   130  		if iNew := modifier(iOld); iNew != Deletion {
   131  			out = out.Append(iNew)
   132  		}
   133  	}
   134  }
   135  
   136  func noopModifier(v any) any { return v }
   137  
   138  func modifyRemaining(in Slice, i int, modifier Modifier, out Slice) (Slice, bool) {
   139  	l := in.Len()
   140  	for ; i < l; i++ {
   141  		before := in.At(i)
   142  		after := modifier(before)
   143  		if out == nil && after == before {
   144  			continue // no changes so far
   145  		}
   146  		if out == nil {
   147  			out = in.CloneShallow(i, l)
   148  		}
   149  		if after != Deletion {
   150  			out = out.Append(after)
   151  		}
   152  	}
   153  	if out == nil {
   154  		return in, false
   155  	}
   156  	return out, true
   157  }