Repository: moby/moby (language: go)

Issue:
docker ps very slow with containerd image store
### Description

When using containerd as the image store, `docker ps` has to query containerd for every container.
Notably, it seems to do this for every single container in the system, not just running ones (which is what `docker ps` shows by default).


### Reproduce

1. Run a few containers (the more, the slower it gets)
2. Run `docker ps`

### Expected behavior

`docker ps` should be a fast operation.

I'm haven't looked at what dockerd is reading from containerd yet, though it is hitting `
containerd.services.content.v1.Content/Info`

Some of the issue is these reads are happening sequentially, so it just takes time to build up the result.
But IMO, we shouldn't have to do this request to containerd at all, especially not for `docker ps`.

### docker version

```bash
Client:
 Version:           24.0.0-beta.1-450-gfa5a0e8139.m
 API version:       1.44
 Go version:        go1.21.1
 Git commit:        fa5a0e8139
 Built:             Sun Oct  1 19:58:37 2023
 OS/Arch:           linux/amd64
 Context:           default

Server:
 Engine:
  Version:          dev
  API version:      1.45 (minimum version 1.24)
  Go version:       go1.22.0
  Git commit:       be0a184c46ad56adc4ff367d2c4db5c530cd80d5
  Built:            Fri Mar 15 14:44:45 2024
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.7.14-1
  GitCommit:        dcf2847247e18caba8dce86522029642f60fe96b
 runc:
  Version:          1.1.12-1
  GitCommit:        51d5e94601ceffbbd85688df1c928ecccbfa4685
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```


### docker info

```bash
Client:
 Version:    24.0.0-beta.1-450-gfa5a0e8139.m
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  0.13.0-1
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  2.24.7-1
    Path:     /usr/libexec/docker/cli-plugins/docker-compose

Server:
 Containers: 11
  Running: 3
  Paused: 0
  Stopped: 8
 Images: 68
 Server Version: dev
 Storage Driver: overlayfs
  driver-type: io.containerd.snapshotter.v1
 Logging Driver: journald
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc runc-patched
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: dcf2847247e18caba8dce86522029642f60fe96b
 runc version: 51d5e94601ceffbbd85688df1c928ecccbfa4685
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 6.5.0-1016-azure
 Operating System: Ubuntu 22.04.4 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 8
 Total Memory: 31.33GiB
 Name: dev2
 ID: ccb953ef-cff6-4d73-94de-24e41bbff5c2
 Docker Root Dir: /var/lib/docker
 Debug Mode: true
  File Descriptors: 50
  Goroutines: 76
  System Time: 2024-03-18T17:13:00.175001129Z
  EventsListeners: 0
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false
```


### Additional Info

Tracing data: [traces-1710782075653.json](https://github.com/moby/moby/files/14640104/traces-1710782075653.json)

In this case I have 3 running containers and 11 total containers.
<img width="703" alt="image" src="https://github.com/moby/moby/assets/799078/de1c2507-656a-4bd0-8037-c2ec53fba622">

We've made 89 requests to containerd to produce this result.


Here is with `docker ps -a` with the same container set:

<img width="701" alt="image" src="https://github.com/moby/moby/assets/799078/53192b53-4c59-40f7-8f2f-58f35c691def">





Discussion / hints:
cc @vvoland @laurazard @rumpl 
Just to add a bit of info, `docker ps` calls `refreshImage` for each container https://github.com/moby/moby/blob/641e341eeda6b54f88b8e5b927db381d1f26164e/daemon/list.go#L135 which calls `GetImage` on the image service https://github.com/moby/moby/blob/641e341eeda6b54f88b8e5b927db381d1f26164e/daemon/list.go#L597
Did a quick profiling and yes, `docker ps` with the containerd image store is slower than without it.

Average `docker ps` latency with 50 running containers on a Linux Ubuntu host with 16 CPU cores and 32GB RAM:

```
With Docker Image Store: 0.018 secs.
With Containerd Image Store: 0.25 secs
```

Thus `docker ps` is ~14x slower when using the containerd image store when running 50 containers.

Furthermore, the `docker ps` latency with the containerd image store scales linearly with the number of containers, where as it remains fairly constant with the Docker image store.

I'll take a look to see how we can improve it.




Here's a PR that improves performance of `docker ps` by parallelizing the way it collects container info from the underlying image store: https://github.com/moby/moby/pull/49365.

It reduces the latency from O(N) to O(log2(N)), which helps a bit when running large number of containers, particularly with the containerd image store. 

Fix patch:
diff --git a/daemon/list.go b/daemon/list.go
index 7fa66b2dd08fd..00373dd9aa0d6 100644
--- a/daemon/list.go
+++ b/daemon/list.go
@@ -3,9 +3,11 @@ package daemon // import "github.com/docker/docker/daemon"
 import (
 	"context"
 	"fmt"
+	"math"
 	"sort"
 	"strconv"
 	"strings"
+	"sync"
 
 	"github.com/containerd/log"
 	"github.com/docker/docker/api/types/backend"
@@ -16,6 +18,7 @@ import (
 	"github.com/docker/docker/image"
 	"github.com/docker/go-connections/nat"
 	"github.com/pkg/errors"
+	"golang.org/x/sync/errgroup"
 )
 
 var acceptedPsFilterTags = map[string]bool{
@@ -103,55 +106,88 @@ func (daemon *Daemon) Containers(ctx context.Context, config *containertypes.Lis
 		return nil, err
 	}
 
-	var (
-		view       = daemon.containersReplica.Snapshot()
-		containers = []*containertypes.Summary{}
-	)
-
+	view := daemon.containersReplica.Snapshot()
 	filter, err := daemon.foldFilter(ctx, view, config)
 	if err != nil {
 		return nil, err
 	}
 
-	// fastpath to only look at a subset of containers if specific name
+	// shortcut to only look at a subset of containers if specific name
 	// or ID matches were provided by the user--otherwise we potentially
 	// end up querying many more containers than intended
+	//
+	// TODO (thaJeztah): given containersReplica.Snapshot() provides a "consistent read-only view of the database" (which indicates "de-referenced copy", is there any reason we wouldn't use a []*container.Snapshot (pointer-slice)?.
 	containerList, err := daemon.filterByNameIDMatches(view, filter)
 	if err != nil {
 		return nil, err
 	}
+	numContainers := len(containerList)
+
+	// shortcut if there are no containers
+	if numContainers == 0 {
+		return []*containertypes.Summary{}, nil
+	}
+
+	// Get the info for each container in the list; this can be slow so we
+	// dispatch a set number of worker goroutines to do the jobs. We choose
+	// log2(numContainers) workers to avoid creating too many goroutines
+	// for large number of containers.
+	numWorkers := int(math.Log2(float64(numContainers)))
+	if numWorkers < 1 {
+		numWorkers = 1
+	}
+
+	resultsMut := sync.Mutex{}
+	results := make([]*containertypes.Summary, numContainers)
+
+	g, ctx := errgroup.WithContext(ctx)
+	g.SetLimit(numWorkers)
 
 	for i := range containerList {
 		currentContainer := &containerList[i]
+		stop := false
 		switch includeContainerInList(currentContainer, filter) {
 		case excludeContainer:
 			continue
 		case stopIteration:
-			return containers, nil
+			stop = true
 		case includeContainer:
-			// transform internal container struct into api structs
-			newC, err := daemon.refreshImage(ctx, currentContainer)
-			if err != nil {
-				return nil, err
-			}
-
-			// release lock because size calculation is slow
-			if filter.Size {
-				sizeRw, sizeRootFs, err := daemon.imageService.GetContainerLayerSize(ctx, newC.ID)
-				if err != nil {
-					return nil, err
+			idx := filter.idx
+			filter.idx++
+
+			g.Go(func() error {
+				// refresh the container image info (in case the image changed in
+				// the repository)
+				newC := daemon.refreshImage(ctx, currentContainer)
+
+				// get the image size (calculation is slow)
+				if filter.Size {
+					var err error
+					newC.SizeRw, newC.SizeRootFs, err = daemon.imageService.GetContainerLayerSize(ctx, newC.ID)
+					if err != nil {
+						return err
+					}
 				}
-				newC.SizeRw = sizeRw
-				newC.SizeRootFs = sizeRootFs
-			}
-			if newC != nil {
-				containers = append(containers, newC)
-				filter.idx++
-			}
+
+				// insert the result at the given index (so the output is in the
+				// same order as containerList above).
+				resultsMut.Lock()
+				results[idx] = newC
+				resultsMut.Unlock()
+
+				return nil
+			})
 		}
+		if stop {
+			break
+		}
+	}
+
+	if err := g.Wait(); err != nil {
+		return nil, err
 	}
 
-	return containers, nil
+	return results[:filter.idx], nil
 }
 
 func (daemon *Daemon) filterByNameIDMatches(view *container.View, filter *listContext) ([]container.Snapshot, error) {
@@ -166,6 +202,7 @@ func (daemon *Daemon) filterByNameIDMatches(view *container.View, filter *listCo
 		if err != nil {
 			return nil, err
 		}
+
 		sort.Sort(byCreatedDescending(all))
 		return all, nil
 	}
@@ -343,9 +380,10 @@ func idOrNameFilter(view *container.View, value string) (*container.Snapshot, er
 	if err != nil && errdefs.IsNotFound(err) {
 		// Try name search instead
 		found := ""
+		searchName := strings.TrimPrefix(value, "/")
 		for id, idNames := range view.GetAllNames() {
-			for _, eachName := range idNames {
-				if strings.TrimPrefix(value, "/") == strings.TrimPrefix(eachName, "/") {
+			for _, name := range idNames {
+				if searchName == strings.TrimPrefix(name, "/") {
 					if found != "" && found != id {
 						return nil, err
 					}
@@ -575,7 +613,7 @@ func includeContainerInList(container *container.Snapshot, filter *listContext)
 // $ docker ps -a
 // CONTAINER ID   IMAGE          COMMAND   CREATED       STATUS                  PORTS     NAMES
 // b0318bca5aef   3fbc63216742   "sh"      3 years ago   Exited (0) 3 years ago            ecstatic_beaver
-func (daemon *Daemon) refreshImage(ctx context.Context, s *container.Snapshot) (*containertypes.Summary, error) {
+func (daemon *Daemon) refreshImage(ctx context.Context, s *container.Snapshot) *containertypes.Summary {
 	c := s.Summary
 
 	// s.Image is the image reference passed by the user to create an image
@@ -589,7 +627,7 @@ func (daemon *Daemon) refreshImage(ctx context.Context, s *container.Snapshot) (
 
 	// If these match, there's nothing to refresh.
 	if s.Image == s.ImageID {
-		return &c, nil
+		return &c
 	}
 
 	// Check if the image reference still resolves to the same digest.
@@ -607,7 +645,7 @@ func (daemon *Daemon) refreshImage(ctx context.Context, s *container.Snapshot) (
 			}).Warn("failed to resolve container image")
 		}
 		c.Image = s.ImageID
-		return &c, nil
+		return &c
 	}
 
 	// Also update the image to the specific image ID, if the Image now
@@ -616,7 +654,7 @@ func (daemon *Daemon) refreshImage(ctx context.Context, s *container.Snapshot) (
 		c.Image = s.ImageID
 	}
 
-	return &c, nil
+	return &c
 }
 
 func populateImageFilterByParents(ctx context.Context, ancestorMap map[image.ID]bool, imageID image.ID, getChildren func(context.Context, image.ID) ([]image.ID, error)) error {
diff --git a/integration/internal/container/container.go b/integration/internal/container/container.go
index 06d3b414f5567..bd14264b88120 100644
--- a/integration/internal/container/container.go
+++ b/integration/internal/container/container.go
@@ -162,6 +162,17 @@ func Remove(ctx context.Context, t *testing.T, apiClient client.APIClient, conta
 	assert.NilError(t, err)
 }
 
+func RemoveAll(ctx context.Context, t *testing.T, apiClient client.APIClient) {
+	t.Helper()
+
+	containers, err := apiClient.ContainerList(ctx, container.ListOptions{All: true})
+	assert.NilError(t, err)
+
+	for _, c := range containers {
+		Remove(ctx, t, apiClient, c.ID, container.RemoveOptions{Force: true})
+	}
+}
+
 func Inspect(ctx context.Context, t *testing.T, apiClient client.APIClient, containerRef string) container.InspectResponse {
 	t.Helper()
 


Test patch:
diff --git a/daemon/list_test.go b/daemon/list_test.go
index cb2b6e8589905..cdf035ee31c95 100644
--- a/daemon/list_test.go
+++ b/daemon/list_test.go
@@ -2,9 +2,11 @@ package daemon
 
 import (
 	"context"
+	"fmt"
 	"os"
 	"path/filepath"
 	"testing"
+	"time"
 
 	containertypes "github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/filters"
@@ -25,7 +27,6 @@ func TestMain(m *testing.M) {
 		panic(err)
 	}
 	defer os.RemoveAll(root)
-
 	os.Exit(m.Run())
 }
 
@@ -51,6 +52,7 @@ func setupContainerWithName(t *testing.T, name string, daemon *Daemon) *containe
 	c.Name = name
 	c.Running = true
 	c.HostConfig = &containertypes.HostConfig{}
+	c.Created = time.Now()
 
 	// these are for passing the refreshImage reducer
 	c.ImageID = computedImageID
@@ -79,7 +81,48 @@ func containerListContainsName(containers []*containertypes.Summary, name string
 	return false
 }
 
-func TestListInvalidFilter(t *testing.T) {
+func TestContainerList(t *testing.T) {
+	db, err := container.NewViewDB()
+	assert.NilError(t, err)
+	d := &Daemon{
+		containersReplica: db,
+	}
+
+	// test list with different number of containers
+	for _, num := range []int{0, 1, 2, 4, 8, 16, 32, 64, 100} {
+		t.Run(fmt.Sprintf("%d containers", num), func(t *testing.T) {
+			db, err := container.NewViewDB() // new DB to ignore prior containers
+			assert.NilError(t, err)
+			d = &Daemon{
+				containersReplica: db,
+			}
+
+			// create the containers
+			containers := make([]*container.Container, num)
+			for i := range num {
+				name := fmt.Sprintf("cont-%d", i)
+				containers[i] = setupContainerWithName(t, name, d)
+				// ensure container timestamps are separated enough so the
+				// sort used by d.Containers() can deterministically sort them.
+				if i > 0 {
+					containers[i].Created = containers[i-1].Created.Add(time.Millisecond)
+				}
+			}
+
+			// list them and verify correctness
+			containerList, err := d.Containers(context.Background(), &containertypes.ListOptions{All: true})
+			assert.NilError(t, err)
+			assert.Assert(t, is.Len(containerList, num))
+
+			for i := range num {
+				// container list should be ordered in descending creation order
+				assert.Assert(t, is.Equal(containerList[i].Names[0], containers[num-1-i].Name))
+			}
+		})
+	}
+}
+
+func TestContainerList_InvalidFilter(t *testing.T) {
 	db, err := container.NewViewDB()
 	assert.NilError(t, err)
 	d := &Daemon{
@@ -92,7 +135,7 @@ func TestListInvalidFilter(t *testing.T) {
 	assert.Assert(t, is.Error(err, "invalid filter 'invalid'"))
 }
 
-func TestNameFilter(t *testing.T) {
+func TestContainerList_NameFilter(t *testing.T) {
 	db, err := container.NewViewDB()
 	assert.NilError(t, err)
 	d := &Daemon{
@@ -140,3 +183,46 @@ func TestNameFilter(t *testing.T) {
 	assert.Assert(t, is.Len(containerListWithPrefix, 1))
 	assert.Assert(t, containerListContainsName(containerListWithPrefix, three.Name))
 }
+
+func TestContainerList_LimitFilter(t *testing.T) {
+	db, err := container.NewViewDB()
+	assert.NilError(t, err)
+	d := &Daemon{
+		containersReplica: db,
+	}
+
+	// start containers
+	num := 32
+	for i := range num {
+		name := fmt.Sprintf("cont-%d", i)
+		setupContainerWithName(t, name, d)
+	}
+
+	containers, err := db.Snapshot().All()
+	assert.NilError(t, err)
+	assert.Assert(t, is.Len(containers, num))
+
+	tests := []struct {
+		limit int
+		doc   string
+	}{
+		{limit: 0, doc: "no limit"},
+		{limit: -1, doc: "negative limit doesn't limit"},
+		{limit: 1, doc: "limit 1 container"},
+		{limit: 20, doc: "limit less than num containers"},
+		{limit: 32, doc: "limit equal num containers"},
+		{limit: 40, doc: "limit greater than num containers"},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.doc, func(t *testing.T) {
+			containerList, err := d.Containers(context.Background(), &containertypes.ListOptions{Limit: tc.limit})
+			assert.NilError(t, err)
+			expectedListLen := num
+			if tc.limit > 0 {
+				expectedListLen = min(num, tc.limit)
+			}
+			assert.Assert(t, is.Len(containerList, expectedListLen))
+		})
+	}
+}
diff --git a/integration/container/list_test.go b/integration/container/list_test.go
index 6b8ab8a378e34..0f88fbd2518d2 100644
--- a/integration/container/list_test.go
+++ b/integration/container/list_test.go
@@ -8,12 +8,40 @@ import (
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/integration/internal/container"
+	"github.com/docker/docker/testutil"
 	"github.com/docker/docker/testutil/request"
 	"gotest.tools/v3/assert"
 	is "gotest.tools/v3/assert/cmp"
+	"gotest.tools/v3/skip"
 )
 
-func TestListAnnotations(t *testing.T) {
+func TestContainerList(t *testing.T) {
+	ctx := setupTest(t)
+	apiClient := request.NewAPIClient(t)
+
+	// remove all existing containers
+	container.RemoveAll(ctx, t, apiClient)
+
+	// create the containers
+	num := 64
+	containers := make([]string, num)
+	for i := range num {
+		id := container.Create(ctx, t, apiClient)
+		defer container.Remove(ctx, t, apiClient, id, containertypes.RemoveOptions{Force: true})
+		containers[i] = id
+	}
+
+	// list them and verify correctness
+	containerList, err := apiClient.ContainerList(ctx, containertypes.ListOptions{All: true})
+	assert.NilError(t, err)
+	assert.Assert(t, is.Len(containerList, num))
+	for i := range num {
+		// container list should be ordered in descending creation order
+		assert.Assert(t, is.Equal(containerList[i].ID, containers[num-1-i]))
+	}
+}
+
+func TestContainerList_Annotations(t *testing.T) {
 	ctx := setupTest(t)
 
 	annotations := map[string]string{
@@ -45,3 +73,73 @@ func TestListAnnotations(t *testing.T) {
 		})
 	}
 }
+
+func TestContainerList_Filter(t *testing.T) {
+	ctx := setupTest(t)
+	apiClient := testEnv.APIClient()
+
+	prev := container.Create(ctx, t, apiClient)
+	top := container.Create(ctx, t, apiClient)
+	next := container.Create(ctx, t, apiClient)
+
+	defer func() {
+		container.Remove(ctx, t, apiClient, prev, containertypes.RemoveOptions{Force: true})
+		container.Remove(ctx, t, apiClient, top, containertypes.RemoveOptions{Force: true})
+		container.Remove(ctx, t, apiClient, next, containertypes.RemoveOptions{Force: true})
+	}()
+
+	containerIDs := func(containers []containertypes.Summary) []string {
+		var entries []string
+		for _, c := range containers {
+			entries = append(entries, c.ID)
+		}
+		return entries
+	}
+
+	t.Run("since", func(t *testing.T) {
+		ctx := testutil.StartSpan(ctx, t)
+		results, err := apiClient.ContainerList(ctx, containertypes.ListOptions{
+			All:     true,
+			Filters: filters.NewArgs(filters.Arg("since", top)),
+		})
+		assert.NilError(t, err)
+		assert.Check(t, is.Contains(containerIDs(results), next))
+	})
+
+	t.Run("before", func(t *testing.T) {
+		ctx := testutil.StartSpan(ctx, t)
+		results, err := apiClient.ContainerList(ctx, containertypes.ListOptions{
+			All:     true,
+			Filters: filters.NewArgs(filters.Arg("before", top)),
+		})
+		assert.NilError(t, err)
+		assert.Check(t, is.Contains(containerIDs(results), prev))
+	})
+}
+
+// TestListPlatform verifies that containers have a platform set
+func TestContainerList_ImageManifestPlatform(t *testing.T) {
+	skip.If(t, testEnv.IsRemoteDaemon)
+	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
+	skip.If(t, !testEnv.UsingSnapshotter())
+
+	ctx := setupTest(t)
+	apiClient := testEnv.APIClient()
+
+	id := container.Create(ctx, t, apiClient)
+	defer container.Remove(ctx, t, apiClient, id, containertypes.RemoveOptions{Force: true})
+
+	containers, err := apiClient.ContainerList(ctx, containertypes.ListOptions{
+		All: true,
+	})
+	assert.NilError(t, err)
+	assert.Assert(t, len(containers) > 0)
+
+	ctr := containers[0]
+	if assert.Check(t, ctr.ImageManifestDescriptor != nil && ctr.ImageManifestDescriptor.Platform != nil) {
+		// Check that at least OS and Architecture have a value. Other values
+		// depend on the platform on which we're running the test.
+		assert.Equal(t, ctr.ImageManifestDescriptor.Platform.OS, testEnv.DaemonInfo.OSType)
+		assert.Check(t, ctr.ImageManifestDescriptor.Platform.Architecture != "")
+	}
+}
diff --git a/integration/container/ps_test.go b/integration/container/ps_test.go
deleted file mode 100644
index 7e617869d0d95..0000000000000
--- a/integration/container/ps_test.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package container // import "github.com/docker/docker/integration/container"
-
-import (
-	"testing"
-
-	containertypes "github.com/docker/docker/api/types/container"
-	"github.com/docker/docker/api/types/filters"
-	"github.com/docker/docker/integration/internal/container"
-	"github.com/docker/docker/testutil"
-	"gotest.tools/v3/assert"
-	is "gotest.tools/v3/assert/cmp"
-	"gotest.tools/v3/skip"
-)
-
-func TestPsFilter(t *testing.T) {
-	ctx := setupTest(t)
-	apiClient := testEnv.APIClient()
-
-	prev := container.Create(ctx, t, apiClient)
-	top := container.Create(ctx, t, apiClient)
-	next := container.Create(ctx, t, apiClient)
-
-	containerIDs := func(containers []containertypes.Summary) []string {
-		var entries []string
-		for _, c := range containers {
-			entries = append(entries, c.ID)
-		}
-		return entries
-	}
-
-	t.Run("since", func(t *testing.T) {
-		ctx := testutil.StartSpan(ctx, t)
-		results, err := apiClient.ContainerList(ctx, containertypes.ListOptions{
-			All:     true,
-			Filters: filters.NewArgs(filters.Arg("since", top)),
-		})
-		assert.NilError(t, err)
-		assert.Check(t, is.Contains(containerIDs(results), next))
-	})
-
-	t.Run("before", func(t *testing.T) {
-		ctx := testutil.StartSpan(ctx, t)
-		results, err := apiClient.ContainerList(ctx, containertypes.ListOptions{
-			All:     true,
-			Filters: filters.NewArgs(filters.Arg("before", top)),
-		})
-		assert.NilError(t, err)
-		assert.Check(t, is.Contains(containerIDs(results), prev))
-	})
-}
-
-// TestPsPlatform verifies that containers have a platform set
-func TestPsImageManifestPlatform(t *testing.T) {
-	skip.If(t, testEnv.IsRemoteDaemon)
-	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
-	skip.If(t, !testEnv.UsingSnapshotter())
-
-	ctx := setupTest(t)
-	apiClient := testEnv.APIClient()
-
-	container.Create(ctx, t, apiClient)
-
-	containers, err := apiClient.ContainerList(ctx, containertypes.ListOptions{
-		All: true,
-	})
-	assert.NilError(t, err)
-	assert.Check(t, len(containers) > 0)
-
-	ctr := containers[0]
-	if assert.Check(t, ctr.ImageManifestDescriptor != nil && ctr.ImageManifestDescriptor.Platform != nil) {
-		// Check that at least OS and Architecture have a value. Other values
-		// depend on the platform on which we're running the test.
-		assert.Equal(t, ctr.ImageManifestDescriptor.Platform.OS, testEnv.DaemonInfo.OSType)
-		assert.Check(t, ctr.ImageManifestDescriptor.Platform.Architecture != "")
-	}
-}