[libcamera-devel,RFC] test: camera: Camera reconfiguration and fd-leak test
diff mbox series

Message ID 20210806115132.353831-1-umang.jain@ideasonboard.com
State Superseded
Delegated to: Umang Jain
Headers show
Series
  • [libcamera-devel,RFC] test: camera: Camera reconfiguration and fd-leak test
Related show

Commit Message

Umang Jain Aug. 6, 2021, 11:51 a.m. UTC
This tests basically checks for two things:
- Camera reconfigurations without stopping CameraManager
- Fd leaks across IPA IPC boundary.

Currently, it uses vimc, but can be easily changed to using another
platform (for e.g. IPU3) by changing CAM_ID and IPA_PROXY_NAME defines.

This test is geared towards reproducing [1], issue noticed on IPU3.

Ensure that test is run in isolated mode.
Running standalone:
$ LIBCAMERA_IPA_FORCE_ISOLATION=1 ./build/test/camera/camera_reconfigure

[1] https://bugs.libcamera.org/show_bug.cgi?id=63

Signed-off-by: Umang Jain <umang.jain@ideasonboard.com>
---
- still RFC because it's based on "Pass buffers to VIMC IPA" series
- We probably need to set LIBCAMERA_IPA_FORCE_ISOLATION=1 from inside
  the test. Setting at run-time with setenv(), sets the env var but still
  the test is unable to run in isolated mode. For now, I only have it
  working, by running it standalone as mentioned in the commit message.
  OR any other ideas enforcing isolated mode on the test?
- Some sprinkled prints are intentionally present, will be reworked in
  v1. Please ignore those for review.
  (log snippet: https://paste.debian.net/1206765/)
---
 test/camera/camera_reconfigure.cpp | 250 +++++++++++++++++++++++++++++
 test/camera/meson.build            |   1 +
 2 files changed, 251 insertions(+)
 create mode 100644 test/camera/camera_reconfigure.cpp

Comments

Paul Elder Aug. 10, 2021, 7:35 a.m. UTC | #1
Hi Umang,

On Fri, Aug 06, 2021 at 05:21:32PM +0530, Umang Jain wrote:
> This tests basically checks for two things:
> - Camera reconfigurations without stopping CameraManager
> - Fd leaks across IPA IPC boundary.

The test looks good!

> 
> Currently, it uses vimc, but can be easily changed to using another
> platform (for e.g. IPU3) by changing CAM_ID and IPA_PROXY_NAME defines.
> 
> This test is geared towards reproducing [1], issue noticed on IPU3.
> 
> Ensure that test is run in isolated mode.
> Running standalone:
> $ LIBCAMERA_IPA_FORCE_ISOLATION=1 ./build/test/camera/camera_reconfigure
> 
> [1] https://bugs.libcamera.org/show_bug.cgi?id=63
> 
> Signed-off-by: Umang Jain <umang.jain@ideasonboard.com>
> ---
> - still RFC because it's based on "Pass buffers to VIMC IPA" series
> - We probably need to set LIBCAMERA_IPA_FORCE_ISOLATION=1 from inside
>   the test. Setting at run-time with setenv(), sets the env var but still
>   the test is unable to run in isolated mode. For now, I only have it

This doesn't work because the IPAs are created very early on, usually at
match() time.

>   working, by running it standalone as mentioned in the commit message.
>   OR any other ideas enforcing isolated mode on the test?

Maybe you could extend the CameraTest class to add an additional extra-early
overridden init() (earlyInit() ?) before cm_ = new CameraManager?

> - Some sprinkled prints are intentionally present, will be reworked in
>   v1. Please ignore those for review.

I'll withold my rb-tag then :D


Thanks,

Paul

>   (log snippet: https://paste.debian.net/1206765/)
> ---
>  test/camera/camera_reconfigure.cpp | 250 +++++++++++++++++++++++++++++
>  test/camera/meson.build            |   1 +
>  2 files changed, 251 insertions(+)
>  create mode 100644 test/camera/camera_reconfigure.cpp
> 
> diff --git a/test/camera/camera_reconfigure.cpp b/test/camera/camera_reconfigure.cpp
> new file mode 100644
> index 00000000..20a4a6a4
> --- /dev/null
> +++ b/test/camera/camera_reconfigure.cpp
> @@ -0,0 +1,250 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2021, Google Inc.
> + *
> + * Test:
> + * - Camera's multiple reconfigurations without stopping CameraManager
> + * - leaking fds across IPA IPC boundary
> + */
> +
> +#include <dirent.h>
> +#include <iostream>
> +#include <fstream>
> +
> +#include <libcamera/framebuffer_allocator.h>
> +
> +#include <libcamera/base/event_dispatcher.h>
> +#include <libcamera/base/file.h>
> +#include <libcamera/base/thread.h>
> +#include <libcamera/base/timer.h>
> +
> +#include "camera_test.h"
> +#include "test.h"
> +
> +using namespace std;
> +
> +#define CAM_ID			"platform/vimc.0 Sensor B"
> +#define IPA_PROXY_NAME		"vimc_ipa_proxy"
> +#define NUM_OF_RECONFIGURES	20
> +
> +namespace {
> +
> +class CameraReconfigure : public CameraTest, public Test
> +{
> +public:
> +	CameraReconfigure()
> +		: CameraTest(CAM_ID)
> +	{
> +	}
> +
> +protected:
> +	unsigned int configureRuns_;
> +	unsigned int openFdsAtStart_;
> +	string vimcProxyName_;
> +	bool allocated_;
> +
> +	vector<unique_ptr<Request>> requests_;
> +
> +	unique_ptr<CameraConfiguration> config_;
> +	FrameBufferAllocator *allocator_;
> +
> +	void requestComplete(Request *request)
> +	{
> +		if (request->status() != Request::RequestComplete)
> +			return;
> +
> +		const Request::BufferMap &buffers = request->buffers();
> +
> +		/* Create a new request. */
> +		const Stream *stream = buffers.begin()->first;
> +		FrameBuffer *buffer = buffers.begin()->second;
> +
> +		request->reuse();
> +		request->addBuffer(stream, buffer);
> +		camera_->queueRequest(request);
> +	}
> +
> +	int startAndStop ()
> +	{
> +		StreamConfiguration &cfg = config_->at(0);
> +
> +		if (camera_->acquire()) {
> +			cout << "Failed to acquire the camera" << endl;
> +			return TestFail;
> +		}
> +
> +		if (camera_->configure(config_.get())) {
> +			cout << "Failed to set default configuration" << endl;
> +			return TestFail;
> +		}
> +
> +		Stream *stream = cfg.stream();
> +
> +		if (!allocated_) {
> +			int ret = allocator_->allocate(stream);
> +			if (ret < 0)
> +				return TestFail;
> +			allocated_ = true;
> +		}
> +
> +		for (const unique_ptr<FrameBuffer> &buffer : allocator_->buffers(stream)) {
> +			unique_ptr<Request> request = camera_->createRequest();
> +			if (!request) {
> +				cout << "Failed to create request" << endl;
> +				return TestFail;
> +			}
> +
> +			if (request->addBuffer(stream, buffer.get())) {
> +				cout << "Failed to associating buffer with request" << endl;
> +				return TestFail;
> +			}
> +
> +			requests_.push_back(move(request));
> +		}
> +
> +		camera_->requestCompleted.connect(this, &CameraReconfigure::requestComplete);
> +
> +		if (camera_->start()) {
> +			cout << "Failed to start camera" << endl;
> +			return TestFail;
> +		}
> +
> +		for (unique_ptr<Request> &request : requests_) {
> +			if (camera_->queueRequest(request.get())) {
> +				cout << "Failed to queue request" << endl;
> +				return TestFail;
> +			}
> +		}
> +
> +		EventDispatcher *dispatcher = Thread::current()->eventDispatcher();
> +
> +		Timer timer;
> +		timer.start(1000);
> +		while (timer.isRunning())
> +			dispatcher->processEvents();
> +
> +		if (camera_->stop()) {
> +			cout << "Failed to stop camera" << endl;
> +			return TestFail;
> +		}
> +
> +		if (camera_->release()) {
> +			cout << "Failed to release camera" << endl;
> +			return TestFail;
> +		}
> +
> +		camera_->requestCompleted.disconnect(this, &CameraReconfigure::requestComplete);
> +
> +		requests_.clear();
> +
> +		return 0;
> +	}
> +
> +	int fdsOpen(string pid)
> +	{
> +		string proxyFdPath = "/proc/" + pid + "/fd";
> +		DIR *dir;
> +		struct dirent *ptr;
> +		unsigned int openFds = 0;
> +
> +		dir = opendir(proxyFdPath.c_str());
> +		while((ptr = readdir(dir)) != nullptr) {
> +			if ((strcmp(ptr->d_name, ".") == 0) || (strcmp(ptr->d_name, "..") == 0))
> +				continue;
> +
> +			openFds++;
> +		}
> +		closedir(dir);
> +
> +		return openFds;
> +	}
> +
> +	string findProxyPid()
> +	{
> +		string proxyPid;
> +		DIR *dir;
> +		struct dirent *ptr;
> +
> +		dir = opendir("/proc");
> +		while((ptr = readdir(dir)) != nullptr) {
> +			if(ptr->d_type != DT_DIR)
> +				continue;
> +
> +			string pname("/proc/" + string(ptr->d_name) + "/comm");
> +			if(File::exists(pname.c_str())) {
> +				ifstream pfile(pname.c_str());
> +				string comm;
> +				getline (pfile, comm);
> +				pfile.close();
> +
> +				proxyPid = comm == vimcProxyName_ ?
> +					   string(ptr->d_name) :
> +					   "";
> +			}
> +
> +			if (!proxyPid.empty())
> +				break;
> +		}
> +		closedir(dir);
> +
> +		return proxyPid;
> +	}
> +
> +	int init() override
> +	{
> +		if (status_ != TestPass)
> +			return status_;
> +
> +		config_ = camera_->generateConfiguration({ StreamRole::StillCapture });
> +		if (!config_ || config_->size() != 1) {
> +			cout << "Failed to generate default configuration" << endl;
> +			return TestFail;
> +		}
> +
> +		allocator_ = new FrameBufferAllocator(camera_);
> +
> +		openFdsAtStart_ = 0;
> +		configureRuns_ = NUM_OF_RECONFIGURES;
> +		vimcProxyName_ = IPA_PROXY_NAME;
> +		allocated_ = false;
> +
> +		return TestPass;
> +	}
> +
> +	void cleanup() override
> +	{
> +		delete allocator_;
> +	}
> +
> +	int run() override
> +	{
> +		unsigned int openFds = 0;
> +		/* Find pid of vimc_ipa_proxy */
> +		string proxyPid = findProxyPid();
> +		if (proxyPid.empty()) {
> +			cout << "Not running in isolated mode, exiting" << endl;
> +			return TestFail;
> +		}
> +
> +		/* Find number of open fds  by vimc_ipa_proxy */
> +		openFdsAtStart_ = fdsOpen(proxyPid);
> +		cout << IPA_PROXY_NAME << " PID:" << proxyPid
> +			<< " has open fds at start:"  << openFdsAtStart_ << endl;
> +
> +		for (unsigned int i = 0; i < configureRuns_; i++)
> +		{
> +			startAndStop();
> +			openFds = fdsOpen(proxyPid);
> +			cout << "No. of open fds after #" << i << " runs: " << openFds << endl;
> +		}
> +
> +		if (openFds == openFdsAtStart_)
> +			return TestPass;
> +		else
> +			return TestFail;
> +	}
> +};
> +
> +} /* namespace */
> +
> +TEST_REGISTER(CameraReconfigure)
> diff --git a/test/camera/meson.build b/test/camera/meson.build
> index 002a87b5..668d5c03 100644
> --- a/test/camera/meson.build
> +++ b/test/camera/meson.build
> @@ -8,6 +8,7 @@ camera_tests = [
>      ['buffer_import',           'buffer_import.cpp'],
>      ['statemachine',            'statemachine.cpp'],
>      ['capture',                 'capture.cpp'],
> +    ['camera_reconfigure',      'camera_reconfigure.cpp'],
>  ]
>  
>  foreach t : camera_tests
> -- 
> 2.31.1
>
Laurent Pinchart Aug. 14, 2021, 9:15 p.m. UTC | #2
Hello,

On Tue, Aug 10, 2021 at 04:35:02PM +0900, paul.elder@ideasonboard.com wrote:
> On Fri, Aug 06, 2021 at 05:21:32PM +0530, Umang Jain wrote:
> > This tests basically checks for two things:
> > - Camera reconfigurations without stopping CameraManager
> > - Fd leaks across IPA IPC boundary.
> 
> The test looks good!
> 
> > Currently, it uses vimc, but can be easily changed to using another
> > platform (for e.g. IPU3) by changing CAM_ID and IPA_PROXY_NAME defines.
> > 
> > This test is geared towards reproducing [1], issue noticed on IPU3.
> > 
> > Ensure that test is run in isolated mode.
> > Running standalone:
> > $ LIBCAMERA_IPA_FORCE_ISOLATION=1 ./build/test/camera/camera_reconfigure
> > 
> > [1] https://bugs.libcamera.org/show_bug.cgi?id=63
> > 
> > Signed-off-by: Umang Jain <umang.jain@ideasonboard.com>
> > ---
> > - still RFC because it's based on "Pass buffers to VIMC IPA" series
> > - We probably need to set LIBCAMERA_IPA_FORCE_ISOLATION=1 from inside
> >   the test. Setting at run-time with setenv(), sets the env var but still
> >   the test is unable to run in isolated mode. For now, I only have it
> 
> This doesn't work because the IPAs are created very early on, usually at
> match() time.

Which happens when CameraManager::start() is called, so it should be
possible to set the environment variable before that.

> >   working, by running it standalone as mentioned in the commit message.
> >   OR any other ideas enforcing isolated mode on the test?
> 
> Maybe you could extend the CameraTest class to add an additional extra-early
> overridden init() (earlyInit() ?) before cm_ = new CameraManager?

Or add a parameter to the constructor.

> > - Some sprinkled prints are intentionally present, will be reworked in
> >   v1. Please ignore those for review.
> 
> I'll withold my rb-tag then :D
> 
> >   (log snippet: https://paste.debian.net/1206765/)
> > ---
> >  test/camera/camera_reconfigure.cpp | 250 +++++++++++++++++++++++++++++
> >  test/camera/meson.build            |   1 +
> >  2 files changed, 251 insertions(+)
> >  create mode 100644 test/camera/camera_reconfigure.cpp
> > 
> > diff --git a/test/camera/camera_reconfigure.cpp b/test/camera/camera_reconfigure.cpp
> > new file mode 100644
> > index 00000000..20a4a6a4
> > --- /dev/null
> > +++ b/test/camera/camera_reconfigure.cpp
> > @@ -0,0 +1,250 @@
> > +/* SPDX-License-Identifier: GPL-2.0-or-later */
> > +/*
> > + * Copyright (C) 2021, Google Inc.
> > + *
> > + * Test:
> > + * - Camera's multiple reconfigurations without stopping CameraManager
> > + * - leaking fds across IPA IPC boundary
> > + */
> > +
> > +#include <dirent.h>
> > +#include <iostream>
> > +#include <fstream>
> > +
> > +#include <libcamera/framebuffer_allocator.h>
> > +
> > +#include <libcamera/base/event_dispatcher.h>
> > +#include <libcamera/base/file.h>
> > +#include <libcamera/base/thread.h>
> > +#include <libcamera/base/timer.h>
> > +
> > +#include "camera_test.h"
> > +#include "test.h"
> > +
> > +using namespace std;
> > +
> > +#define CAM_ID			"platform/vimc.0 Sensor B"
> > +#define IPA_PROXY_NAME		"vimc_ipa_proxy"
> > +#define NUM_OF_RECONFIGURES	20

20 runs of one second each is long compared to the time taken to run the
test suite today. Can this be sped up ?

The C++ way would be

static constexpr char *kCamId = "platform/vimc.0 Sensor B";
static constexpr char *kIpaProxyName = "vimc_ipa_proxy";
static constexpr unsigned int kNumOfReconfigures = 20;

They can also be made members of the CameraReconfigure class.

> > +
> > +namespace {
> > +
> > +class CameraReconfigure : public CameraTest, public Test
> > +{
> > +public:
> > +	CameraReconfigure()
> > +		: CameraTest(CAM_ID)
> > +	{
> > +	}
> > +
> > +protected:
> > +	unsigned int configureRuns_;
> > +	unsigned int openFdsAtStart_;
> > +	string vimcProxyName_;
> > +	bool allocated_;
> > +
> > +	vector<unique_ptr<Request>> requests_;
> > +
> > +	unique_ptr<CameraConfiguration> config_;
> > +	FrameBufferAllocator *allocator_;

Variables afer functions please. And these variables should be private.

> > +
> > +	void requestComplete(Request *request)
> > +	{
> > +		if (request->status() != Request::RequestComplete)
> > +			return;
> > +
> > +		const Request::BufferMap &buffers = request->buffers();
> > +
> > +		/* Create a new request. */
> > +		const Stream *stream = buffers.begin()->first;
> > +		FrameBuffer *buffer = buffers.begin()->second;
> > +
> > +		request->reuse();
> > +		request->addBuffer(stream, buffer);
> > +		camera_->queueRequest(request);
> > +	}
> > +
> > +	int startAndStop ()

Extra space.

> > +	{
> > +		StreamConfiguration &cfg = config_->at(0);
> > +
> > +		if (camera_->acquire()) {
> > +			cout << "Failed to acquire the camera" << endl;
> > +			return TestFail;
> > +		}
> > +
> > +		if (camera_->configure(config_.get())) {
> > +			cout << "Failed to set default configuration" << endl;
> > +			return TestFail;
> > +		}
> > +
> > +		Stream *stream = cfg.stream();
> > +
> > +		if (!allocated_) {
> > +			int ret = allocator_->allocate(stream);
> > +			if (ret < 0)
> > +				return TestFail;
> > +			allocated_ = true;
> > +		}
> > +
> > +		for (const unique_ptr<FrameBuffer> &buffer : allocator_->buffers(stream)) {
> > +			unique_ptr<Request> request = camera_->createRequest();
> > +			if (!request) {
> > +				cout << "Failed to create request" << endl;
> > +				return TestFail;
> > +			}
> > +
> > +			if (request->addBuffer(stream, buffer.get())) {
> > +				cout << "Failed to associating buffer with request" << endl;
> > +				return TestFail;
> > +			}
> > +
> > +			requests_.push_back(move(request));
> > +		}
> > +
> > +		camera_->requestCompleted.connect(this, &CameraReconfigure::requestComplete);
> > +
> > +		if (camera_->start()) {
> > +			cout << "Failed to start camera" << endl;
> > +			return TestFail;
> > +		}
> > +
> > +		for (unique_ptr<Request> &request : requests_) {
> > +			if (camera_->queueRequest(request.get())) {
> > +				cout << "Failed to queue request" << endl;
> > +				return TestFail;
> > +			}
> > +		}
> > +
> > +		EventDispatcher *dispatcher = Thread::current()->eventDispatcher();
> > +
> > +		Timer timer;
> > +		timer.start(1000);
> > +		while (timer.isRunning())
> > +			dispatcher->processEvents();
> > +
> > +		if (camera_->stop()) {
> > +			cout << "Failed to stop camera" << endl;
> > +			return TestFail;
> > +		}
> > +
> > +		if (camera_->release()) {
> > +			cout << "Failed to release camera" << endl;
> > +			return TestFail;
> > +		}
> > +
> > +		camera_->requestCompleted.disconnect(this, &CameraReconfigure::requestComplete);
> > +
> > +		requests_.clear();
> > +
> > +		return 0;
> > +	}
> > +
> > +	int fdsOpen(string pid)
> > +	{
> > +		string proxyFdPath = "/proc/" + pid + "/fd";
> > +		DIR *dir;
> > +		struct dirent *ptr;
> > +		unsigned int openFds = 0;
> > +
> > +		dir = opendir(proxyFdPath.c_str());

Error checking ?

> > +		while((ptr = readdir(dir)) != nullptr) {

s/while/while /

There are other missing spaces below.

> > +			if ((strcmp(ptr->d_name, ".") == 0) || (strcmp(ptr->d_name, "..") == 0))
> > +				continue;
> > +
> > +			openFds++;
> > +		}
> > +		closedir(dir);
> > +
> > +		return openFds;
> > +	}
> > +
> > +	string findProxyPid()

A pid is a number, I'd return a pid_t.

> > +	{
> > +		string proxyPid;
> > +		DIR *dir;
> > +		struct dirent *ptr;
> > +
> > +		dir = opendir("/proc");
> > +		while((ptr = readdir(dir)) != nullptr) {
> > +			if(ptr->d_type != DT_DIR)
> > +				continue;
> > +
> > +			string pname("/proc/" + string(ptr->d_name) + "/comm");
> > +			if(File::exists(pname.c_str())) {
> > +				ifstream pfile(pname.c_str());
> > +				string comm;
> > +				getline (pfile, comm);
> > +				pfile.close();
> > +
> > +				proxyPid = comm == vimcProxyName_ ?
> > +					   string(ptr->d_name) :
> > +					   "";
> > +			}
> > +
> > +			if (!proxyPid.empty())
> > +				break;
> > +		}
> > +		closedir(dir);
> > +
> > +		return proxyPid;
> > +	}
> > +
> > +	int init() override
> > +	{
> > +		if (status_ != TestPass)
> > +			return status_;
> > +
> > +		config_ = camera_->generateConfiguration({ StreamRole::StillCapture });
> > +		if (!config_ || config_->size() != 1) {
> > +			cout << "Failed to generate default configuration" << endl;
> > +			return TestFail;
> > +		}
> > +
> > +		allocator_ = new FrameBufferAllocator(camera_);
> > +
> > +		openFdsAtStart_ = 0;
> > +		configureRuns_ = NUM_OF_RECONFIGURES;
> > +		vimcProxyName_ = IPA_PROXY_NAME;
> > +		allocated_ = false;
> > +
> > +		return TestPass;
> > +	}
> > +
> > +	void cleanup() override
> > +	{
> > +		delete allocator_;

You can store it in an std::unique_ptr<>.

> > +	}
> > +
> > +	int run() override
> > +	{
> > +		unsigned int openFds = 0;
> > +		/* Find pid of vimc_ipa_proxy */
> > +		string proxyPid = findProxyPid();
> > +		if (proxyPid.empty()) {
> > +			cout << "Not running in isolated mode, exiting" << endl;
> > +			return TestFail;
> > +		}
> > +
> > +		/* Find number of open fds  by vimc_ipa_proxy */
> > +		openFdsAtStart_ = fdsOpen(proxyPid);
> > +		cout << IPA_PROXY_NAME << " PID:" << proxyPid
> > +			<< " has open fds at start:"  << openFdsAtStart_ << endl;
> > +
> > +		for (unsigned int i = 0; i < configureRuns_; i++)

You can drop the configureRuns_ member variable and use
kNumOfReconfigures directly here. Same for the proxy name I think.

> > +		{

This should be at the end of the previous line.

> > +			startAndStop();
> > +			openFds = fdsOpen(proxyPid);
> > +			cout << "No. of open fds after #" << i << " runs: " << openFds << endl;
> > +		}
> > +
> > +		if (openFds == openFdsAtStart_)

Shouldn't this be checked for every run of the loop ? I'd then print the
message with the number of fds only on failure.

> > +			return TestPass;
> > +		else
> > +			return TestFail;

I wonder if counting the number of open fds at exit time isn't something
that the VIMC IPA should implement. That way, we would have the check in
every test, not just this one.

Paul, what do you think ?

> > +	}
> > +};
> > +
> > +} /* namespace */
> > +
> > +TEST_REGISTER(CameraReconfigure)
> > diff --git a/test/camera/meson.build b/test/camera/meson.build
> > index 002a87b5..668d5c03 100644
> > --- a/test/camera/meson.build
> > +++ b/test/camera/meson.build
> > @@ -8,6 +8,7 @@ camera_tests = [
> >      ['buffer_import',           'buffer_import.cpp'],
> >      ['statemachine',            'statemachine.cpp'],
> >      ['capture',                 'capture.cpp'],
> > +    ['camera_reconfigure',      'camera_reconfigure.cpp'],
> >  ]
> >  
> >  foreach t : camera_tests
Paul Elder Aug. 16, 2021, 6:54 a.m. UTC | #3
Hi Laurent,

On Sun, Aug 15, 2021 at 12:15:25AM +0300, Laurent Pinchart wrote:
> Hello,
> 
> On Tue, Aug 10, 2021 at 04:35:02PM +0900, paul.elder@ideasonboard.com wrote:
> > On Fri, Aug 06, 2021 at 05:21:32PM +0530, Umang Jain wrote:
> > > This tests basically checks for two things:
> > > - Camera reconfigurations without stopping CameraManager
> > > - Fd leaks across IPA IPC boundary.
> > 
> > The test looks good!
> > 
> > > Currently, it uses vimc, but can be easily changed to using another
> > > platform (for e.g. IPU3) by changing CAM_ID and IPA_PROXY_NAME defines.
> > > 
> > > This test is geared towards reproducing [1], issue noticed on IPU3.
> > > 
> > > Ensure that test is run in isolated mode.
> > > Running standalone:
> > > $ LIBCAMERA_IPA_FORCE_ISOLATION=1 ./build/test/camera/camera_reconfigure
> > > 
> > > [1] https://bugs.libcamera.org/show_bug.cgi?id=63
> > > 
> > > Signed-off-by: Umang Jain <umang.jain@ideasonboard.com>
> > > ---
> > > - still RFC because it's based on "Pass buffers to VIMC IPA" series
> > > - We probably need to set LIBCAMERA_IPA_FORCE_ISOLATION=1 from inside
> > >   the test. Setting at run-time with setenv(), sets the env var but still
> > >   the test is unable to run in isolated mode. For now, I only have it
> > 
> > This doesn't work because the IPAs are created very early on, usually at
> > match() time.
> 
> Which happens when CameraManager::start() is called, so it should be
> possible to set the environment variable before that.
> 
> > >   working, by running it standalone as mentioned in the commit message.
> > >   OR any other ideas enforcing isolated mode on the test?
> > 
> > Maybe you could extend the CameraTest class to add an additional extra-early
> > overridden init() (earlyInit() ?) before cm_ = new CameraManager?
> 
> Or add a parameter to the constructor.
> 
> > > - Some sprinkled prints are intentionally present, will be reworked in
> > >   v1. Please ignore those for review.
> > 
> > I'll withold my rb-tag then :D
> > 
> > >   (log snippet: https://paste.debian.net/1206765/)
> > > ---
> > >  test/camera/camera_reconfigure.cpp | 250 +++++++++++++++++++++++++++++
> > >  test/camera/meson.build            |   1 +
> > >  2 files changed, 251 insertions(+)
> > >  create mode 100644 test/camera/camera_reconfigure.cpp
> > > 
> > > diff --git a/test/camera/camera_reconfigure.cpp b/test/camera/camera_reconfigure.cpp
> > > new file mode 100644
> > > index 00000000..20a4a6a4
> > > --- /dev/null
> > > +++ b/test/camera/camera_reconfigure.cpp
> > > @@ -0,0 +1,250 @@
> > > +/* SPDX-License-Identifier: GPL-2.0-or-later */
> > > +/*
> > > + * Copyright (C) 2021, Google Inc.
> > > + *
> > > + * Test:
> > > + * - Camera's multiple reconfigurations without stopping CameraManager
> > > + * - leaking fds across IPA IPC boundary
> > > + */
> > > +
> > > +#include <dirent.h>
> > > +#include <iostream>
> > > +#include <fstream>
> > > +
> > > +#include <libcamera/framebuffer_allocator.h>
> > > +
> > > +#include <libcamera/base/event_dispatcher.h>
> > > +#include <libcamera/base/file.h>
> > > +#include <libcamera/base/thread.h>
> > > +#include <libcamera/base/timer.h>
> > > +
> > > +#include "camera_test.h"
> > > +#include "test.h"
> > > +
> > > +using namespace std;
> > > +
> > > +#define CAM_ID			"platform/vimc.0 Sensor B"
> > > +#define IPA_PROXY_NAME		"vimc_ipa_proxy"
> > > +#define NUM_OF_RECONFIGURES	20
> 
> 20 runs of one second each is long compared to the time taken to run the
> test suite today. Can this be sped up ?
> 
> The C++ way would be
> 
> static constexpr char *kCamId = "platform/vimc.0 Sensor B";
> static constexpr char *kIpaProxyName = "vimc_ipa_proxy";
> static constexpr unsigned int kNumOfReconfigures = 20;
> 
> They can also be made members of the CameraReconfigure class.
> 
> > > +
> > > +namespace {
> > > +
> > > +class CameraReconfigure : public CameraTest, public Test
> > > +{
> > > +public:
> > > +	CameraReconfigure()
> > > +		: CameraTest(CAM_ID)
> > > +	{
> > > +	}
> > > +
> > > +protected:
> > > +	unsigned int configureRuns_;
> > > +	unsigned int openFdsAtStart_;
> > > +	string vimcProxyName_;
> > > +	bool allocated_;
> > > +
> > > +	vector<unique_ptr<Request>> requests_;
> > > +
> > > +	unique_ptr<CameraConfiguration> config_;
> > > +	FrameBufferAllocator *allocator_;
> 
> Variables afer functions please. And these variables should be private.
> 
> > > +
> > > +	void requestComplete(Request *request)
> > > +	{
> > > +		if (request->status() != Request::RequestComplete)
> > > +			return;
> > > +
> > > +		const Request::BufferMap &buffers = request->buffers();
> > > +
> > > +		/* Create a new request. */
> > > +		const Stream *stream = buffers.begin()->first;
> > > +		FrameBuffer *buffer = buffers.begin()->second;
> > > +
> > > +		request->reuse();
> > > +		request->addBuffer(stream, buffer);
> > > +		camera_->queueRequest(request);
> > > +	}
> > > +
> > > +	int startAndStop ()
> 
> Extra space.
> 
> > > +	{
> > > +		StreamConfiguration &cfg = config_->at(0);
> > > +
> > > +		if (camera_->acquire()) {
> > > +			cout << "Failed to acquire the camera" << endl;
> > > +			return TestFail;
> > > +		}
> > > +
> > > +		if (camera_->configure(config_.get())) {
> > > +			cout << "Failed to set default configuration" << endl;
> > > +			return TestFail;
> > > +		}
> > > +
> > > +		Stream *stream = cfg.stream();
> > > +
> > > +		if (!allocated_) {
> > > +			int ret = allocator_->allocate(stream);
> > > +			if (ret < 0)
> > > +				return TestFail;
> > > +			allocated_ = true;
> > > +		}
> > > +
> > > +		for (const unique_ptr<FrameBuffer> &buffer : allocator_->buffers(stream)) {
> > > +			unique_ptr<Request> request = camera_->createRequest();
> > > +			if (!request) {
> > > +				cout << "Failed to create request" << endl;
> > > +				return TestFail;
> > > +			}
> > > +
> > > +			if (request->addBuffer(stream, buffer.get())) {
> > > +				cout << "Failed to associating buffer with request" << endl;
> > > +				return TestFail;
> > > +			}
> > > +
> > > +			requests_.push_back(move(request));
> > > +		}
> > > +
> > > +		camera_->requestCompleted.connect(this, &CameraReconfigure::requestComplete);
> > > +
> > > +		if (camera_->start()) {
> > > +			cout << "Failed to start camera" << endl;
> > > +			return TestFail;
> > > +		}
> > > +
> > > +		for (unique_ptr<Request> &request : requests_) {
> > > +			if (camera_->queueRequest(request.get())) {
> > > +				cout << "Failed to queue request" << endl;
> > > +				return TestFail;
> > > +			}
> > > +		}
> > > +
> > > +		EventDispatcher *dispatcher = Thread::current()->eventDispatcher();
> > > +
> > > +		Timer timer;
> > > +		timer.start(1000);
> > > +		while (timer.isRunning())
> > > +			dispatcher->processEvents();
> > > +
> > > +		if (camera_->stop()) {
> > > +			cout << "Failed to stop camera" << endl;
> > > +			return TestFail;
> > > +		}
> > > +
> > > +		if (camera_->release()) {
> > > +			cout << "Failed to release camera" << endl;
> > > +			return TestFail;
> > > +		}
> > > +
> > > +		camera_->requestCompleted.disconnect(this, &CameraReconfigure::requestComplete);
> > > +
> > > +		requests_.clear();
> > > +
> > > +		return 0;
> > > +	}
> > > +
> > > +	int fdsOpen(string pid)
> > > +	{
> > > +		string proxyFdPath = "/proc/" + pid + "/fd";
> > > +		DIR *dir;
> > > +		struct dirent *ptr;
> > > +		unsigned int openFds = 0;
> > > +
> > > +		dir = opendir(proxyFdPath.c_str());
> 
> Error checking ?
> 
> > > +		while((ptr = readdir(dir)) != nullptr) {
> 
> s/while/while /
> 
> There are other missing spaces below.
> 
> > > +			if ((strcmp(ptr->d_name, ".") == 0) || (strcmp(ptr->d_name, "..") == 0))
> > > +				continue;
> > > +
> > > +			openFds++;
> > > +		}
> > > +		closedir(dir);
> > > +
> > > +		return openFds;
> > > +	}
> > > +
> > > +	string findProxyPid()
> 
> A pid is a number, I'd return a pid_t.
> 
> > > +	{
> > > +		string proxyPid;
> > > +		DIR *dir;
> > > +		struct dirent *ptr;
> > > +
> > > +		dir = opendir("/proc");
> > > +		while((ptr = readdir(dir)) != nullptr) {
> > > +			if(ptr->d_type != DT_DIR)
> > > +				continue;
> > > +
> > > +			string pname("/proc/" + string(ptr->d_name) + "/comm");
> > > +			if(File::exists(pname.c_str())) {
> > > +				ifstream pfile(pname.c_str());
> > > +				string comm;
> > > +				getline (pfile, comm);
> > > +				pfile.close();
> > > +
> > > +				proxyPid = comm == vimcProxyName_ ?
> > > +					   string(ptr->d_name) :
> > > +					   "";
> > > +			}
> > > +
> > > +			if (!proxyPid.empty())
> > > +				break;
> > > +		}
> > > +		closedir(dir);
> > > +
> > > +		return proxyPid;
> > > +	}
> > > +
> > > +	int init() override
> > > +	{
> > > +		if (status_ != TestPass)
> > > +			return status_;
> > > +
> > > +		config_ = camera_->generateConfiguration({ StreamRole::StillCapture });
> > > +		if (!config_ || config_->size() != 1) {
> > > +			cout << "Failed to generate default configuration" << endl;
> > > +			return TestFail;
> > > +		}
> > > +
> > > +		allocator_ = new FrameBufferAllocator(camera_);
> > > +
> > > +		openFdsAtStart_ = 0;
> > > +		configureRuns_ = NUM_OF_RECONFIGURES;
> > > +		vimcProxyName_ = IPA_PROXY_NAME;
> > > +		allocated_ = false;
> > > +
> > > +		return TestPass;
> > > +	}
> > > +
> > > +	void cleanup() override
> > > +	{
> > > +		delete allocator_;
> 
> You can store it in an std::unique_ptr<>.
> 
> > > +	}
> > > +
> > > +	int run() override
> > > +	{
> > > +		unsigned int openFds = 0;
> > > +		/* Find pid of vimc_ipa_proxy */
> > > +		string proxyPid = findProxyPid();
> > > +		if (proxyPid.empty()) {
> > > +			cout << "Not running in isolated mode, exiting" << endl;
> > > +			return TestFail;
> > > +		}
> > > +
> > > +		/* Find number of open fds  by vimc_ipa_proxy */
> > > +		openFdsAtStart_ = fdsOpen(proxyPid);
> > > +		cout << IPA_PROXY_NAME << " PID:" << proxyPid
> > > +			<< " has open fds at start:"  << openFdsAtStart_ << endl;
> > > +
> > > +		for (unsigned int i = 0; i < configureRuns_; i++)
> 
> You can drop the configureRuns_ member variable and use
> kNumOfReconfigures directly here. Same for the proxy name I think.
> 
> > > +		{
> 
> This should be at the end of the previous line.
> 
> > > +			startAndStop();
> > > +			openFds = fdsOpen(proxyPid);
> > > +			cout << "No. of open fds after #" << i << " runs: " << openFds << endl;
> > > +		}
> > > +
> > > +		if (openFds == openFdsAtStart_)
> 
> Shouldn't this be checked for every run of the loop ? I'd then print the
> message with the number of fds only on failure.
> 
> > > +			return TestPass;
> > > +		else
> > > +			return TestFail;
> 
> I wonder if counting the number of open fds at exit time isn't something
> that the VIMC IPA should implement. That way, we would have the check in
> every test, not just this one.
> 
> Paul, what do you think ?

Yeah, I think that would be useful.


Paul

> 
> > > +	}
> > > +};
> > > +
> > > +} /* namespace */
> > > +
> > > +TEST_REGISTER(CameraReconfigure)
> > > diff --git a/test/camera/meson.build b/test/camera/meson.build
> > > index 002a87b5..668d5c03 100644
> > > --- a/test/camera/meson.build
> > > +++ b/test/camera/meson.build
> > > @@ -8,6 +8,7 @@ camera_tests = [
> > >      ['buffer_import',           'buffer_import.cpp'],
> > >      ['statemachine',            'statemachine.cpp'],
> > >      ['capture',                 'capture.cpp'],
> > > +    ['camera_reconfigure',      'camera_reconfigure.cpp'],
> > >  ]
> > >  
> > >  foreach t : camera_tests

Patch
diff mbox series

diff --git a/test/camera/camera_reconfigure.cpp b/test/camera/camera_reconfigure.cpp
new file mode 100644
index 00000000..20a4a6a4
--- /dev/null
+++ b/test/camera/camera_reconfigure.cpp
@@ -0,0 +1,250 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2021, Google Inc.
+ *
+ * Test:
+ * - Camera's multiple reconfigurations without stopping CameraManager
+ * - leaking fds across IPA IPC boundary
+ */
+
+#include <dirent.h>
+#include <iostream>
+#include <fstream>
+
+#include <libcamera/framebuffer_allocator.h>
+
+#include <libcamera/base/event_dispatcher.h>
+#include <libcamera/base/file.h>
+#include <libcamera/base/thread.h>
+#include <libcamera/base/timer.h>
+
+#include "camera_test.h"
+#include "test.h"
+
+using namespace std;
+
+#define CAM_ID			"platform/vimc.0 Sensor B"
+#define IPA_PROXY_NAME		"vimc_ipa_proxy"
+#define NUM_OF_RECONFIGURES	20
+
+namespace {
+
+class CameraReconfigure : public CameraTest, public Test
+{
+public:
+	CameraReconfigure()
+		: CameraTest(CAM_ID)
+	{
+	}
+
+protected:
+	unsigned int configureRuns_;
+	unsigned int openFdsAtStart_;
+	string vimcProxyName_;
+	bool allocated_;
+
+	vector<unique_ptr<Request>> requests_;
+
+	unique_ptr<CameraConfiguration> config_;
+	FrameBufferAllocator *allocator_;
+
+	void requestComplete(Request *request)
+	{
+		if (request->status() != Request::RequestComplete)
+			return;
+
+		const Request::BufferMap &buffers = request->buffers();
+
+		/* Create a new request. */
+		const Stream *stream = buffers.begin()->first;
+		FrameBuffer *buffer = buffers.begin()->second;
+
+		request->reuse();
+		request->addBuffer(stream, buffer);
+		camera_->queueRequest(request);
+	}
+
+	int startAndStop ()
+	{
+		StreamConfiguration &cfg = config_->at(0);
+
+		if (camera_->acquire()) {
+			cout << "Failed to acquire the camera" << endl;
+			return TestFail;
+		}
+
+		if (camera_->configure(config_.get())) {
+			cout << "Failed to set default configuration" << endl;
+			return TestFail;
+		}
+
+		Stream *stream = cfg.stream();
+
+		if (!allocated_) {
+			int ret = allocator_->allocate(stream);
+			if (ret < 0)
+				return TestFail;
+			allocated_ = true;
+		}
+
+		for (const unique_ptr<FrameBuffer> &buffer : allocator_->buffers(stream)) {
+			unique_ptr<Request> request = camera_->createRequest();
+			if (!request) {
+				cout << "Failed to create request" << endl;
+				return TestFail;
+			}
+
+			if (request->addBuffer(stream, buffer.get())) {
+				cout << "Failed to associating buffer with request" << endl;
+				return TestFail;
+			}
+
+			requests_.push_back(move(request));
+		}
+
+		camera_->requestCompleted.connect(this, &CameraReconfigure::requestComplete);
+
+		if (camera_->start()) {
+			cout << "Failed to start camera" << endl;
+			return TestFail;
+		}
+
+		for (unique_ptr<Request> &request : requests_) {
+			if (camera_->queueRequest(request.get())) {
+				cout << "Failed to queue request" << endl;
+				return TestFail;
+			}
+		}
+
+		EventDispatcher *dispatcher = Thread::current()->eventDispatcher();
+
+		Timer timer;
+		timer.start(1000);
+		while (timer.isRunning())
+			dispatcher->processEvents();
+
+		if (camera_->stop()) {
+			cout << "Failed to stop camera" << endl;
+			return TestFail;
+		}
+
+		if (camera_->release()) {
+			cout << "Failed to release camera" << endl;
+			return TestFail;
+		}
+
+		camera_->requestCompleted.disconnect(this, &CameraReconfigure::requestComplete);
+
+		requests_.clear();
+
+		return 0;
+	}
+
+	int fdsOpen(string pid)
+	{
+		string proxyFdPath = "/proc/" + pid + "/fd";
+		DIR *dir;
+		struct dirent *ptr;
+		unsigned int openFds = 0;
+
+		dir = opendir(proxyFdPath.c_str());
+		while((ptr = readdir(dir)) != nullptr) {
+			if ((strcmp(ptr->d_name, ".") == 0) || (strcmp(ptr->d_name, "..") == 0))
+				continue;
+
+			openFds++;
+		}
+		closedir(dir);
+
+		return openFds;
+	}
+
+	string findProxyPid()
+	{
+		string proxyPid;
+		DIR *dir;
+		struct dirent *ptr;
+
+		dir = opendir("/proc");
+		while((ptr = readdir(dir)) != nullptr) {
+			if(ptr->d_type != DT_DIR)
+				continue;
+
+			string pname("/proc/" + string(ptr->d_name) + "/comm");
+			if(File::exists(pname.c_str())) {
+				ifstream pfile(pname.c_str());
+				string comm;
+				getline (pfile, comm);
+				pfile.close();
+
+				proxyPid = comm == vimcProxyName_ ?
+					   string(ptr->d_name) :
+					   "";
+			}
+
+			if (!proxyPid.empty())
+				break;
+		}
+		closedir(dir);
+
+		return proxyPid;
+	}
+
+	int init() override
+	{
+		if (status_ != TestPass)
+			return status_;
+
+		config_ = camera_->generateConfiguration({ StreamRole::StillCapture });
+		if (!config_ || config_->size() != 1) {
+			cout << "Failed to generate default configuration" << endl;
+			return TestFail;
+		}
+
+		allocator_ = new FrameBufferAllocator(camera_);
+
+		openFdsAtStart_ = 0;
+		configureRuns_ = NUM_OF_RECONFIGURES;
+		vimcProxyName_ = IPA_PROXY_NAME;
+		allocated_ = false;
+
+		return TestPass;
+	}
+
+	void cleanup() override
+	{
+		delete allocator_;
+	}
+
+	int run() override
+	{
+		unsigned int openFds = 0;
+		/* Find pid of vimc_ipa_proxy */
+		string proxyPid = findProxyPid();
+		if (proxyPid.empty()) {
+			cout << "Not running in isolated mode, exiting" << endl;
+			return TestFail;
+		}
+
+		/* Find number of open fds  by vimc_ipa_proxy */
+		openFdsAtStart_ = fdsOpen(proxyPid);
+		cout << IPA_PROXY_NAME << " PID:" << proxyPid
+			<< " has open fds at start:"  << openFdsAtStart_ << endl;
+
+		for (unsigned int i = 0; i < configureRuns_; i++)
+		{
+			startAndStop();
+			openFds = fdsOpen(proxyPid);
+			cout << "No. of open fds after #" << i << " runs: " << openFds << endl;
+		}
+
+		if (openFds == openFdsAtStart_)
+			return TestPass;
+		else
+			return TestFail;
+	}
+};
+
+} /* namespace */
+
+TEST_REGISTER(CameraReconfigure)
diff --git a/test/camera/meson.build b/test/camera/meson.build
index 002a87b5..668d5c03 100644
--- a/test/camera/meson.build
+++ b/test/camera/meson.build
@@ -8,6 +8,7 @@  camera_tests = [
     ['buffer_import',           'buffer_import.cpp'],
     ['statemachine',            'statemachine.cpp'],
     ['capture',                 'capture.cpp'],
+    ['camera_reconfigure',      'camera_reconfigure.cpp'],
 ]
 
 foreach t : camera_tests