diff --git a/local/store.go b/local/store.go index 87e1d10a..e93a0c69 100644 --- a/local/store.go +++ b/local/store.go @@ -200,6 +200,9 @@ func (s *Store) doSave(img v1.Image, withName string) (image.InspectResponse, er } func (s *Store) addImageToTar(tw *tar.Writer, image v1.Image, withName string) error { + // Check if we can optimize by using known layer sizes + canOptimize := s.canOptimizeLayerSizes(image) + rawConfigFile, err := image.RawConfigFile() if err != nil { return err @@ -217,7 +220,12 @@ func (s *Store) addImageToTar(tw *tar.Writer, image v1.Image, withName string) e blankIdx int ) for _, layer := range layers { - layerName, err := s.addLayerToTar(tw, layer, blankIdx) + var layerName string + if canOptimize { + layerName, err = s.addLayerToTarOptimized(tw, layer, blankIdx) + } else { + layerName, err = s.addLayerToTar(tw, layer, blankIdx) + } if err != nil { return err } @@ -238,6 +246,72 @@ func (s *Store) addImageToTar(tw *tar.Writer, image v1.Image, withName string) e return addTextToTar(tw, manifestJSON, "manifest.json") } +func (s *Store) canOptimizeLayerSizes(image v1.Image) bool { + layers, err := image.Layers() + if err != nil { + return false + } + + // Check if all layers have known uncompressed sizes (i.e., they came from docker save) + for _, layer := range layers { + diffID, err := layer.DiffID() + if err != nil { + return false + } + knownLayer, layerFound := s.onDiskLayersByDiffID[diffID] + if !layerFound || knownLayer.uncompressedSize == -1 { + return false + } + } + + return true +} + +func (s *Store) addLayerToTarOptimized(tw *tar.Writer, layer v1.Layer, blankIdx int) (string, error) { + // This optimized version avoids reading through the layer to calculate uncompressed size + // when we already know it from docker save + + // Get size info from cache first + diffID, err := layer.DiffID() + if err != nil { + return "", err + } + knownLayer := s.onDiskLayersByDiffID[diffID] + + size, err := layer.Size() + if err != nil { + return "", err + } + + var layerName string + if size == -1 { // it's a base (always empty) layer + layerName = fmt.Sprintf("blank_%d", blankIdx) + hdr := &tar.Header{Name: layerName, Mode: 0644, Size: 0} + return layerName, tw.WriteHeader(hdr) + } + + // it's a populated layer - use known uncompressed size + layerName = fmt.Sprintf("/%s.tar", diffID.String()) + uncompressedSize := knownLayer.uncompressedSize + + // Use the uncompressed layer reader + layerReader, err := layer.Uncompressed() + if err != nil { + return "", err + } + defer layerReader.Close() + + hdr := &tar.Header{Name: layerName, Mode: 0644, Size: uncompressedSize} + if err = tw.WriteHeader(hdr); err != nil { + return "", err + } + if _, err = io.Copy(tw, layerReader); err != nil { + return "", err + } + + return layerName, nil +} + func (s *Store) addLayerToTar(tw *tar.Writer, layer v1.Layer, blankIdx int) (string, error) { // If the layer is a previous image layer that hasn't been downloaded yet, // cause ALL the previous image layers to be downloaded by grabbing the ReadCloser. @@ -281,8 +355,8 @@ func (s *Store) addLayerToTar(tw *tar.Writer, layer v1.Layer, blankIdx int) (str // getLayerSize returns the uncompressed layer size. // This is needed because the daemon expects uncompressed layer size and a v1.Layer reports compressed layer size; -// in a future where we send OCI layout tars to the daemon we should be able to remove this method -// and the need to track layers individually. +// Performance optimization: When all layers have known uncompressed sizes (from docker save), +// use addLayerToTarOptimized to avoid this expensive calculation. func (s *Store) getLayerSize(layer v1.Layer) (int64, error) { diffID, err := layer.DiffID() if err != nil { @@ -292,6 +366,8 @@ func (s *Store) getLayerSize(layer v1.Layer) (int64, error) { if layerFound && knownLayer.uncompressedSize != -1 { return knownLayer.uncompressedSize, nil } + // Performance optimization: Avoid expensive size calculation when all layers are known. + // This happens when layers were downloaded via docker save and have cached uncompressed sizes. // FIXME: this is a time sink and should be avoided if the daemon accepts OCI layout-formatted tars // If layer was not seen previously, we need to read it to get the uncompressed size // In practice, we should only get here if layers saved from the daemon via `docker save`