Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

images: speed up lists #7215

Merged

Conversation

vrothberg
Copy link
Member

@vrothberg vrothberg commented Aug 4, 2020

Listing images has shown increasing performance penalties with an
increasing number of images. Unless --all is specified, Podman
will filter intermediate images. Determining intermediate images
has been done by finding (and comparing!) parent images which is
expensive. We had to query the storage many times which turned it
into a bottleneck.

Instead, create a layer tree and assign one or more images to nodes that
match the images' top layer. Determining the children of an image is
now exponentially faster as we already know the child images from the
layer graph and the images using the same top layer, which may also be
considered child images based on their history.

On my system with 510 images, a rootful image list drops from 6 secs
down to 0.3 secs.

Also use the tree to compute parent nodes, and to filter intermediate
images for pruning.

Signed-off-by: Valentin Rothberg [email protected]

@openshift-ci-robot openshift-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Aug 4, 2020
@mheon
Copy link
Member

mheon commented Aug 4, 2020 via email

@vrothberg
Copy link
Member Author

Caching is very hard to get right for long-running processes as we need to invalidate the caches at some point. Using a variadic func was the lazy alternative to adding new functions with an images []*Image argument. Shall I do that instead?

@mheon
Copy link
Member

mheon commented Aug 4, 2020

@vrothberg That would be a good compromise.

@baude
Copy link
Member

baude commented Aug 4, 2020

lgtm

@vrothberg
Copy link
Member Author

Let's wait for @mtrmac's head nod before merging. The performance diff is almost too good to be true/correct

Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • AFAICS this is not quite the same thing: IsParent matches images that have the same top layer as a “child” but has one fewer history entries than the child.
  • If you are worried about time complexity, the iteration over storageLayers is still O(layers) ≥ O(images) in every isIntermediate call, making this O(images^2); instead, you can build a map[parentLayerId]hasChild in O(layers) and check that for all images in O(images).
  • Does pkg/api/handlers/utils.GetImages need the same change?

Aesthetically, I’m not too much of a fan of adding a separate “quick fix” instead of making the primary parent/child determination code fast. It should AFAICS possible to build a full parent/child tree in something close to O(images+layers), without changing the semantics, by

  • building a layer tree
  • building some kind of “history ID” (sha256sum of sha256sums of (the relevant fields of?) individual history entries?), for the full history of each image, and the the history of each image but the last entry
  • building an image tree on top of these history IDs
  • intersecting the layer and history trees, to get the same semantics as before

Some callers only need a single parent/child response, and those would not be (asymptotically) slower than the current code; the other callers like “remove” and “list” would fairly strongly benefit.

GetLayersMapWithImageInfo is a starting point but does not compute exactly the same thing (i.o.w. podman image tree and podman images list show different trees!).

@openshift-ci-robot
Copy link
Collaborator

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: vrothberg

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@vrothberg vrothberg force-pushed the flatten-the-curve branch 4 times, most recently from 1067f29 to 2b345b1 Compare August 6, 2020 13:04
@vrothberg
Copy link
Member Author

CI was green. Now with intermediate updates.

@baude @mheon @mtrmac PTAL

@vrothberg
Copy link
Member Author

@rhatdan @nalind PTAL

@TomSweeneyRedHat
Copy link
Member

LGTM, would like a head nod from @nalind

Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The areParentAndChild checks are not quite O(images), but I suspect this gets us 99% there in practice, a nice improvement already.

libpod/image/filters.go Outdated Show resolved Hide resolved
libpod/image/image.go Outdated Show resolved Hide resolved
libpod/image/filters.go Show resolved Hide resolved
libpod/image/image.go Show resolved Hide resolved
libpod/image/image.go Outdated Show resolved Hide resolved

// Now assign the images to each (top) layer.
for i := range images {
img := images[i] // do not leak loop variable outside the scope
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this actually make a difference? There isn’t a closure that would survive the loop iteration.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was pretty sure that we need it for node.images = append(node.images, img), don't we?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think so; that line reads the contents of the img variable at that point and appends the pointer to node.images. If img changes later, that doesn’t affect node.images.

The copy/new scope is only necessary when the evaluation of a variable is deferred after the iteration terminates, e.g. runOneSecondLater(func() { consume(img) })

libpod/image/layer_tree.go Show resolved Hide resolved
libpod/image/layer_tree.go Outdated Show resolved Hide resolved
libpod/image/layer_tree.go Outdated Show resolved Hide resolved
libpod/image/layer_tree.go Outdated Show resolved Hide resolved
@vrothberg vrothberg force-pushed the flatten-the-curve branch 2 times, most recently from 4c92ca4 to 9d15e5a Compare August 7, 2020 10:08
Listing images has shown increasing performance penalties with an
increasing number of images.  Unless `--all` is specified, Podman
will filter intermediate images.  Determining intermediate images
has been done by finding (and comparing!) parent images which is
expensive.  We had to query the storage many times which turned it
into a bottleneck.

Instead, create a layer tree and assign one or more images to nodes that
match the images' top layer.  Determining the children of an image is
now exponentially faster as we already know the child images from the
layer graph and the images using the same top layer, which may also be
considered child images based on their history.

On my system with 510 images, a rootful image list drops from 6 secs
down to 0.3 secs.

Also use the tree to compute parent nodes, and to filter intermediate
images for pruning.

Signed-off-by: Valentin Rothberg <[email protected]>
@vrothberg
Copy link
Member Author

Updated. Thanks for the great review!

@mtrmac, I created a Jira card to perform some follow-up work to wire in the layer into more APIs (and to also have a closer look at pkg/api/handlers).

Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

foundChildren = true
children = append(children, childImage.ID())
if all {
return foundChildren, nil
Copy link
Collaborator

@mtrmac mtrmac Aug 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Absolutely non-blocking: A break would be a tiny bit easier to follow for me.)

@rhatdan
Copy link
Member

rhatdan commented Aug 8, 2020

/lgtm
/hold
@vrothberg You can either fix @mtrmac nit in this PR or a follow on PR, you can decide to remove the hold.

@openshift-ci-robot openshift-ci-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Aug 8, 2020
@openshift-ci-robot openshift-ci-robot added the lgtm Indicates that a PR is ready to be merged. label Aug 8, 2020
@vrothberg
Copy link
Member Author

/hold cancel

I can fix it with the follow-up work 👍

@openshift-ci-robot openshift-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Aug 8, 2020
@openshift-merge-robot openshift-merge-robot merged commit 3173a18 into containers:master Aug 8, 2020
@vrothberg vrothberg deleted the flatten-the-curve branch August 9, 2020 08:51
@vrothberg
Copy link
Member Author

@mheon, I'd love to get this into the v2.0 branch. Cool with it?

@github-actions github-actions bot added the locked - please file new issue/PR Assist humans wanting to comment on an old issue or PR with locked comments. label Sep 24, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 24, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. lgtm Indicates that a PR is ready to be merged. locked - please file new issue/PR Assist humans wanting to comment on an old issue or PR with locked comments.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants