Skip to content

Commit debc18a

Browse files
Cache loaded images for performance improvements. (bazelbuild#1934)
Locally on ubuntu 18.04, the join_layers step takes greater than 30 seconds for the container_bundle_with_install_pkgs target without this change, and ~5 seconds with this change. With the previous implementation, join_layers was passing the same set of images to reader.parts for each call to ReadImage. The reader, created fresh for each call to ReadImage, would then load these same images again. This results in a fixed cost for every image you add to the bundle. For large bundles, this can be very costly (on the order of minutes). Updated other callers of compat.ReadImages to use this new API of creating a reader first, then calling reader.ReadImage. Co-authored-by: aptenodytes-forsteri <aptenodytesforstericodes@gmail.com>
1 parent 3929701 commit debc18a

File tree

6 files changed

+84
-38
lines changed

6 files changed

+84
-38
lines changed

container/go/cmd/digester/digester.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ func main() {
4949
if err != nil {
5050
log.Fatalf("Unable to determine parts of the image from the specified arguments: %v", err)
5151
}
52-
img, err := compat.ReadImage(imgParts)
52+
r := compat.Reader{Parts: imgParts}
53+
img, err := r.ReadImage()
5354
if err != nil {
5455
log.Fatalf("Error reading image: %v", err)
5556
}

container/go/cmd/flattener/flattener.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ func main() {
5252
if err != nil {
5353
log.Fatalf("Unable to determine parts of the image from the specified arguments: %v", err)
5454
}
55-
img, err := compat.ReadImage(imgParts)
55+
r := compat.Reader{Parts: imgParts}
56+
img, err := r.ReadImage()
5657
if err != nil {
5758
log.Fatalf("Error reading image: %v", err)
5859
}

container/go/cmd/join_layers/join_layers.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,19 @@ func writeOutput(outputTarball string, tarballFormat string, tagToConfigs, tagTo
8585
if err != nil {
8686
return errors.Wrap(err, "unable to load images from the given tarballs")
8787
}
88+
parts := compat.ImageParts{
89+
Images: images,
90+
Layers: layerParts,
91+
}
92+
r := compat.Reader{Parts: parts}
8893
for tag, configFile := range tagToConfigs {
8994
// Manifest file may not have been specified and this is ok as it's
9095
// only required if the base images has foreign layers.
9196
manifestFile := tagToBaseManifests[tag]
92-
parts := compat.ImageParts{
93-
Config: configFile,
94-
BaseManifest: manifestFile,
95-
Images: images,
96-
Layers: layerParts,
97-
}
98-
img, err := compat.ReadImage(parts)
97+
r.Parts.Config = configFile
98+
r.Parts.BaseManifest = manifestFile
99+
100+
img, err := r.ReadImage()
99101
if err != nil {
100102
return errors.Wrapf(err, "unable to load image %v corresponding to config %s", tag, configFile)
101103
}

container/go/cmd/pusher/pusher.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ func main() {
9999
if err != nil {
100100
log.Fatalf("Unable to determine parts of the image from the specified arguments: %v", err)
101101
}
102-
img, err := compat.ReadImage(imgParts)
102+
r := compat.Reader{Parts: imgParts}
103+
img, err := r.ReadImage()
103104
if err != nil {
104105
log.Fatalf("Error reading image: %v", err)
105106
}

container/go/pkg/compat/reader.go

+40-28
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,11 @@ func ImagePartsFromArgs(config, baseManifest, imgTarball string, layers []string
117117
return result, nil
118118
}
119119

120-
// reader maintains the state necessary to build a legacyImage object from an
120+
// Reader maintains the state necessary to build a legacyImage object from an
121121
// ImageParts object.
122-
type reader struct {
122+
type Reader struct {
123123
// parts is the ImageParts being loaded.
124-
parts ImageParts
124+
Parts ImageParts
125125
// baseManifest is the manifest of the very first base image in the chain
126126
// of images being loaded.
127127
baseManifest *v1.Manifest
@@ -130,32 +130,35 @@ type reader struct {
130130
// layerLookup is a map from the diffID of a layer to the layer
131131
// itself.
132132
layerLookup map[v1.Hash]v1.Layer
133+
// loadedImageCache is a cache of all images that have been loaded into memory,
134+
// to prevent costly reloads.
135+
loadedImageCache map[v1.Hash]bool
133136
}
134137

135138
// loadMetadata loads the image metadata for the image parts in the given
136139
// reader.
137-
func (r *reader) loadMetadata() error {
138-
cf, err := os.Open(r.parts.Config)
140+
func (r *Reader) loadMetadata() error {
141+
cf, err := os.Open(r.Parts.Config)
139142
if err != nil {
140-
return errors.Wrapf(err, "unable to open image config file %s", r.parts.Config)
143+
return errors.Wrapf(err, "unable to open image config file %s", r.Parts.Config)
141144
}
142145
c, err := v1.ParseConfigFile(cf)
143146
if err != nil {
144-
return errors.Wrapf(err, "unable to parse image config from %s", r.parts.Config)
147+
return errors.Wrapf(err, "unable to parse image config from %s", r.Parts.Config)
145148
}
146149
r.config = c
147-
if r.parts.BaseManifest == "" {
150+
if r.Parts.BaseManifest == "" {
148151
// Base manifest is optional. It's only needed for images whose base
149152
// manifests have foreign layers.
150153
return nil
151154
}
152-
mf, err := os.Open(r.parts.BaseManifest)
155+
mf, err := os.Open(r.Parts.BaseManifest)
153156
if err != nil {
154-
return errors.Wrapf(err, "unable to open base image manifest file %s", r.parts.BaseManifest)
157+
return errors.Wrapf(err, "unable to open base image manifest file %s", r.Parts.BaseManifest)
155158
}
156159
m, err := v1.ParseManifest(mf)
157160
if err != nil {
158-
return errors.Wrapf(err, "unable to parse base image manifest from %s", r.parts.BaseManifest)
161+
return errors.Wrapf(err, "unable to parse base image manifest from %s", r.Parts.BaseManifest)
159162
}
160163
r.baseManifest = m
161164
return nil
@@ -209,7 +212,7 @@ func (l *foreignLayer) MediaType() (types.MediaType, error) {
209212

210213
// loadForeignLayers loads the foreign layers from the base manifest in the
211214
// given reader into the layer lookup.
212-
func (r *reader) loadForeignLayers() error {
215+
func (r *Reader) loadForeignLayers() error {
213216
if r.baseManifest == nil {
214217
// No base manifest so no foreign layers to load.
215218
return nil
@@ -237,8 +240,12 @@ func (r *reader) loadForeignLayers() error {
237240

238241
// loadImages loads the layers from the given images into the layers lookup
239242
// in the given reader.
240-
func (r *reader) loadImages(images []v1.Image) error {
243+
func (r *Reader) loadImages(images []v1.Image) error {
241244
for _, img := range images {
245+
digest, _ := img.Digest()
246+
if r.loadedImageCache[digest] {
247+
continue
248+
}
242249
layers, err := img.Layers()
243250
if err != nil {
244251
return errors.Wrap(err, "unable to get the layers in image")
@@ -250,31 +257,32 @@ func (r *reader) loadImages(images []v1.Image) error {
250257
}
251258
r.layerLookup[diffID] = l
252259
}
260+
r.loadedImageCache[digest] = true
253261
}
254262
return nil
255263
}
256264

257265
// loadImgTarball loads the layers from the image tarball in the parts section
258266
// of the given reader if one was specified into the layers lookup in the given
259267
// reader.
260-
func (r *reader) loadImgTarball() error {
261-
if r.parts.ImageTarball == "" {
268+
func (r *Reader) loadImgTarball() error {
269+
if r.Parts.ImageTarball == "" {
262270
return nil
263271
}
264-
img, err := tarball.ImageFromPath(r.parts.ImageTarball, nil)
272+
img, err := tarball.ImageFromPath(r.Parts.ImageTarball, nil)
265273
if err != nil {
266-
return errors.Wrapf(err, "unable to load image from tarball %s", r.parts.ImageTarball)
274+
return errors.Wrapf(err, "unable to load image from tarball %s", r.Parts.ImageTarball)
267275
}
268276
if err := r.loadImages([]v1.Image{img}); err != nil {
269-
return errors.Wrapf(err, "unable to load the layers from image loaded from tarball %s", r.parts.ImageTarball)
277+
return errors.Wrapf(err, "unable to load the layers from image loaded from tarball %s", r.Parts.ImageTarball)
270278
}
271279
return nil
272280
}
273281

274282
// loadLayers loads layers specified as parts in the ImageParts section in the
275283
// given reader.
276-
func (r *reader) loadLayers() error {
277-
for _, l := range r.parts.Layers {
284+
func (r *Reader) loadLayers() error {
285+
for _, l := range r.Parts.Layers {
278286
layer, err := l.V1Layer()
279287
if err != nil {
280288
return errors.Wrap(err, "unable to build a v1.Layer from the specified parts")
@@ -289,23 +297,27 @@ func (r *reader) loadLayers() error {
289297
}
290298

291299
// ReadImage loads a v1.Image from the given ImageParts
292-
func ReadImage(parts ImageParts) (v1.Image, error) {
300+
func (r *Reader) ReadImage() (v1.Image, error) {
293301
// Special case: if we only have a tarball, we can instantiate the image
294302
// directly from that. Otherwise, we'll process the image layers
295303
// individually as specified in the config.
296-
if parts.ImageTarball != "" && parts.Config == "" {
297-
return tarball.ImageFromPath(parts.ImageTarball, nil)
304+
if r.Parts.ImageTarball != "" && r.Parts.Config == "" {
305+
return tarball.ImageFromPath(r.Parts.ImageTarball, nil)
298306
}
299307

300-
r := reader{parts: parts}
301-
r.layerLookup = make(map[v1.Hash]v1.Layer)
308+
if r.layerLookup == nil {
309+
r.layerLookup = make(map[v1.Hash]v1.Layer)
310+
}
311+
if r.loadedImageCache == nil {
312+
r.loadedImageCache = make(map[v1.Hash]bool)
313+
}
302314
if err := r.loadMetadata(); err != nil {
303315
return nil, errors.Wrap(err, "unable to load image metadata")
304316
}
305317
if err := r.loadForeignLayers(); err != nil {
306318
return nil, errors.Wrap(err, "unable to load foreign layers specified in the base manifest")
307319
}
308-
if err := r.loadImages(r.parts.Images); err != nil {
320+
if err := r.loadImages(r.Parts.Images); err != nil {
309321
return nil, errors.Wrap(err, "unable to load layers from the images in the given image parts")
310322
}
311323
if err := r.loadImgTarball(); err != nil {
@@ -318,12 +330,12 @@ func ReadImage(parts ImageParts) (v1.Image, error) {
318330
for _, diffID := range r.config.RootFS.DiffIDs {
319331
layer, ok := r.layerLookup[diffID]
320332
if !ok {
321-
return nil, errors.Errorf("unable to locate layer with diffID %v as indicated in image config %s", diffID, parts.Config)
333+
return nil, errors.Errorf("unable to locate layer with diffID %v as indicated in image config %s", diffID, r.Parts.Config)
322334
}
323335
layers = append(layers, layer)
324336
}
325337
img := &legacyImage{
326-
configPath: parts.Config,
338+
configPath: r.Parts.Config,
327339
layers: layers,
328340
}
329341
if err := img.init(); err != nil {

tests/container/BUILD

+29
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ load(
1717
"@bazel_tools//tools/build_rules:test_rules.bzl",
1818
"file_test",
1919
)
20+
load("@rules_python//python:defs.bzl", "py_test")
2021
load("//container:bundle.bzl", "container_bundle")
2122
load(
2223
"//container:container.bzl",
@@ -969,3 +970,31 @@ py_test(
969970
srcs_version = "PY3",
970971
deps = ["@containerregistry"],
971972
)
973+
974+
container_image(
975+
name = "test_install_pkgs",
976+
base = "//tests/docker/package_managers:test_install_pkgs.tar",
977+
)
978+
979+
container_image(
980+
name = "test_install_git_for_reproducibility_1",
981+
base = "//tests/docker/package_managers:install_git_for_reproducibility_1.tar",
982+
)
983+
984+
# Test a container bundle with several images and install_pkgs.
985+
# This should not tax join_layers unnecessarily.
986+
# Run bazel build @io_bazel_rules_docker//tests/container:container_bundle_with_install_pkgs.tar to test.
987+
# Prior to adding a caching step to compat/reader.go, join_layers.go
988+
# would take substantially longer for this target.
989+
container_bundle(
990+
name = "container_bundle_with_install_pkgs",
991+
images = {
992+
"install_pkgs_1:latest": ":test_install_pkgs",
993+
"install_pkgs_2:latest": ":test_install_git_for_reproducibility_1",
994+
"localhost:5000/image0:latest": "//testdata:base_with_entrypoint",
995+
"localhost:5000/image1:latest": "//testdata:link_with_files_base",
996+
"localhost:5000/image2:latest": "//testdata:with_double_env",
997+
"localhost:5000/image3:latest": "//testdata:with_label",
998+
"localhost:5000/image4:latest": "//testdata:with_double_label",
999+
},
1000+
)

0 commit comments

Comments
 (0)