Skip to content

Commit dbc56a9

Browse files
authored
refactor(perf): optimize cases with no child referrers in recursiveCopy (#1729)
Signed-off-by: Billy Zha <[email protected]>
1 parent 457e342 commit dbc56a9

File tree

2 files changed

+196
-31
lines changed

2 files changed

+196
-31
lines changed

cmd/oras/root/cp.go

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -225,43 +225,63 @@ func doCopy(ctx context.Context, copyHandler status.CopyHandler, src oras.ReadOn
225225
// recursiveCopy copies an artifact and its referrers from one target to another.
226226
// If the artifact is a manifest list or index, referrers of its manifests are copied as well.
227227
func recursiveCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.Target, dstRef string, root ocispec.Descriptor, opts oras.ExtendedCopyOptions) error {
228-
if root.MediaType == ocispec.MediaTypeImageIndex || root.MediaType == docker.MediaTypeManifestList {
229-
fetched, err := content.FetchAll(ctx, src, root)
230-
if err != nil {
231-
return err
232-
}
233-
var index ocispec.Index
234-
if err = json.Unmarshal(fetched, &index); err != nil {
235-
return nil
236-
}
237-
238-
referrers, err := graph.FindPredecessors(ctx, src, index.Manifests, opts)
239-
if err != nil {
240-
return err
241-
}
242-
referrers = slices.DeleteFunc(referrers, func(desc ocispec.Descriptor) bool {
243-
return content.Equal(desc, root)
244-
})
245-
246-
findPredecessor := opts.FindPredecessors
247-
opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
248-
descs, err := findPredecessor(ctx, src, desc)
249-
if err != nil {
250-
return nil, err
251-
}
252-
if content.Equal(desc, root) {
253-
// make sure referrers of child manifests are copied by pointing them to root
254-
descs = append(descs, referrers...)
255-
}
256-
return descs, nil
257-
}
228+
var err error
229+
if opts, err = prepareCopyOption(ctx, src, dst, root, opts); err != nil {
230+
return err
258231
}
259232

260-
var err error
261233
if dstRef == "" || dstRef == root.Digest.String() {
262234
err = oras.ExtendedCopyGraph(ctx, src, dst, root, opts.ExtendedCopyGraphOptions)
263235
} else {
264236
_, err = oras.ExtendedCopy(ctx, src, root.Digest.String(), dst, dstRef, opts)
265237
}
266238
return err
267239
}
240+
241+
func prepareCopyOption(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.Target, root ocispec.Descriptor, opts oras.ExtendedCopyOptions) (oras.ExtendedCopyOptions, error) {
242+
if root.MediaType != ocispec.MediaTypeImageIndex && root.MediaType != docker.MediaTypeManifestList {
243+
return opts, nil
244+
}
245+
246+
fetched, err := content.FetchAll(ctx, src, root)
247+
if err != nil {
248+
return opts, err
249+
}
250+
var index ocispec.Index
251+
if err = json.Unmarshal(fetched, &index); err != nil {
252+
return opts, err
253+
}
254+
255+
if len(index.Manifests) == 0 {
256+
// no child manifests, thus no child referrers
257+
return opts, nil
258+
}
259+
260+
referrers, err := graph.FindPredecessors(ctx, src, index.Manifests, opts)
261+
if err != nil {
262+
return opts, err
263+
}
264+
265+
referrers = slices.DeleteFunc(referrers, func(desc ocispec.Descriptor) bool {
266+
return content.Equal(desc, root)
267+
})
268+
269+
if len(referrers) == 0 {
270+
// no child referrers
271+
return opts, nil
272+
}
273+
274+
findPredecessor := opts.FindPredecessors
275+
opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
276+
descs, err := findPredecessor(ctx, src, desc)
277+
if err != nil {
278+
return nil, err
279+
}
280+
if content.Equal(desc, root) {
281+
// make sure referrers of child manifests are copied by pointing them to root
282+
descs = append(descs, referrers...)
283+
}
284+
return descs, nil
285+
}
286+
return opts, nil
287+
}

cmd/oras/root/cp_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,20 @@ package root
2020
import (
2121
"bytes"
2222
"context"
23+
"encoding/json"
2324
"fmt"
25+
"io"
2426
"net/http"
2527
"net/http/httptest"
2628
"net/url"
2729
"os"
30+
"strings"
2831
"testing"
2932

3033
"github.com/opencontainers/go-digest"
3134
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
35+
"oras.land/oras-go/v2"
36+
"oras.land/oras-go/v2/content"
3237
"oras.land/oras-go/v2/content/memory"
3338
"oras.land/oras-go/v2/registry/remote"
3439
"oras.land/oras/cmd/oras/internal/display/status"
@@ -197,3 +202,143 @@ func Test_doCopy_mounted(t *testing.T) {
197202
t.Fatal(err)
198203
}
199204
}
205+
206+
func Test_prepareCopyOption_nonIndex(t *testing.T) {
207+
ctx := context.Background()
208+
root := ocispec.Descriptor{
209+
MediaType: ocispec.MediaTypeImageManifest,
210+
}
211+
if _, err := prepareCopyOption(ctx, nil, nil, root, oras.ExtendedCopyOptions{}); err != nil {
212+
t.Errorf("prepareCopyOption() error = %v, wantErr false", err)
213+
}
214+
}
215+
216+
var errMockedFetch = fmt.Errorf("fetch error")
217+
218+
// fetchFailingReadOnlyGraphTarget is a mock implementation of oras.ReadOnlyGraphTarget
219+
type fetchFailingReadOnlyGraphTarget struct {
220+
oras.ReadOnlyGraphTarget
221+
}
222+
223+
// Fetch simulates a failure when fetching content from the source.
224+
func (m *fetchFailingReadOnlyGraphTarget) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
225+
return nil, errMockedFetch
226+
}
227+
228+
func Test_prepareCopyOption_fetchFailure(t *testing.T) {
229+
ctx := context.Background()
230+
src := &fetchFailingReadOnlyGraphTarget{}
231+
dst := memory.New()
232+
root := ocispec.Descriptor{
233+
MediaType: ocispec.MediaTypeImageIndex,
234+
Digest: digest.FromString("nonexistent"),
235+
Size: int64(len("nonexistent")),
236+
}
237+
238+
if _, err := prepareCopyOption(ctx, src, dst, root, oras.ExtendedCopyOptions{}); err != errMockedFetch {
239+
t.Errorf("prepareCopyOption() error = %v, want %v", err, errMockedFetch)
240+
}
241+
}
242+
243+
func Test_recursiveCopy_prepareCopyOptionFailure(t *testing.T) {
244+
ctx := context.Background()
245+
src := &fetchFailingReadOnlyGraphTarget{}
246+
dst := memory.New()
247+
root := ocispec.Descriptor{
248+
MediaType: ocispec.MediaTypeImageIndex,
249+
Digest: digest.FromString("nonexistent"),
250+
Size: int64(len("nonexistent")),
251+
}
252+
253+
if _, err := prepareCopyOption(ctx, src, dst, root, oras.ExtendedCopyOptions{}); err != errMockedFetch {
254+
t.Errorf("prepareCopyOption() error = %v, want %v", err, errMockedFetch)
255+
}
256+
}
257+
258+
// invalidJSONReadOnlyGraphTarget is a mock implementation of oras.ReadOnlyGraphTarget
259+
// that returns invalid JSON data to simulate a JSON unmarshalling failure.
260+
type invalidJSONReadOnlyGraphTarget struct {
261+
oras.ReadOnlyGraphTarget
262+
}
263+
264+
// Fetch simulates a successful fetch of invalid JSON data.
265+
func (m *invalidJSONReadOnlyGraphTarget) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
266+
// Return invalid JSON data
267+
return io.NopCloser(strings.NewReader("invalid-json")), nil
268+
}
269+
270+
func Test_prepareCopyOption_jsonUnmarshalFailure(t *testing.T) {
271+
ctx := context.Background()
272+
src := &invalidJSONReadOnlyGraphTarget{}
273+
dst := memory.New()
274+
root := ocispec.Descriptor{
275+
MediaType: ocispec.MediaTypeImageIndex,
276+
Digest: digest.FromString("invalid-json"),
277+
Size: int64(len("invalid-json")),
278+
}
279+
_, err := prepareCopyOption(ctx, src, dst, root, oras.ExtendedCopyOptions{})
280+
if _, ok := err.(*json.SyntaxError); !ok {
281+
t.Errorf("prepareCopyOption() error = %v, want json.SyntaxError", err)
282+
}
283+
}
284+
285+
// mockReferrersFailingSource is a mock implementation of oras.ReadOnlyGraphTarget
286+
// that simulates a failure when fetching referrers.
287+
type mockReferrersFailingSource struct {
288+
oras.ReadOnlyGraphTarget
289+
indexContent string
290+
}
291+
292+
// Fetch simulates successful fetching of index content.
293+
func (m *mockReferrersFailingSource) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
294+
// Return valid JSON data to pass the fetch step
295+
return io.NopCloser(strings.NewReader(m.indexContent)), nil
296+
}
297+
298+
func Test_prepareCopyOption_referrersFailure(t *testing.T) {
299+
300+
ctx := context.Background()
301+
mockedIndex := `{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}]}`
302+
src := &mockReferrersFailingSource{indexContent: mockedIndex}
303+
dst := memory.New()
304+
root := ocispec.Descriptor{
305+
MediaType: ocispec.MediaTypeImageIndex,
306+
Digest: digest.FromString(mockedIndex),
307+
Size: int64(len(mockedIndex)),
308+
}
309+
errMockedReferrers := fmt.Errorf("failed to get referrers")
310+
opts := oras.ExtendedCopyOptions{
311+
ExtendedCopyGraphOptions: oras.ExtendedCopyGraphOptions{
312+
FindPredecessors: func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
313+
return nil, errMockedReferrers
314+
},
315+
},
316+
}
317+
318+
if _, err := prepareCopyOption(ctx, src, dst, root, opts); err != errMockedReferrers {
319+
t.Errorf("prepareCopyOption() error = %v, wantErr %v", err, errMockedReferrers)
320+
}
321+
}
322+
323+
func Test_prepareCopyOption_noReferrers(t *testing.T) {
324+
ctx := context.Background()
325+
mockedIndex := `{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}]}`
326+
src := &mockReferrersFailingSource{indexContent: mockedIndex}
327+
dst := memory.New()
328+
root := ocispec.Descriptor{
329+
MediaType: ocispec.MediaTypeImageIndex,
330+
Digest: digest.FromString(mockedIndex),
331+
Size: int64(len(mockedIndex)),
332+
}
333+
opts := oras.ExtendedCopyOptions{
334+
ExtendedCopyGraphOptions: oras.ExtendedCopyGraphOptions{
335+
FindPredecessors: func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
336+
return nil, nil
337+
},
338+
},
339+
}
340+
341+
if _, err := prepareCopyOption(ctx, src, dst, root, opts); err != nil {
342+
t.Errorf("prepareCopyOption() error = %v, wantErr false", err)
343+
}
344+
}

0 commit comments

Comments
 (0)