diff mbox series

[v1] fuse: optimize over-io-uring request expiration check

Message ID 20250123235251.1139078-1-joannelkoong@gmail.com (mailing list archive)
State New
Headers show
Series [v1] fuse: optimize over-io-uring request expiration check | expand

Commit Message

Joanne Koong Jan. 23, 2025, 11:52 p.m. UTC
Currently, when checking whether a request has timed out, we check
fpq processing, but fuse-over-io-uring has one fpq per core and 256
entries in the processing table. For systems where there are a
large number of cores, this may be too much overhead.

Instead of checking the fpq processing list, check ent_w_req_queue,
ent_in_userspace, and ent_commit_queue.

Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
 fs/fuse/dev.c        |  2 +-
 fs/fuse/dev_uring.c  | 23 ++++++++++++++++++++---
 fs/fuse/fuse_dev_i.h |  1 -
 3 files changed, 21 insertions(+), 5 deletions(-)

Comments

Bernd Schubert Jan. 24, 2025, 11:30 a.m. UTC | #1
On 1/24/25 00:52, Joanne Koong wrote:
> Currently, when checking whether a request has timed out, we check
> fpq processing, but fuse-over-io-uring has one fpq per core and 256
> entries in the processing table. For systems where there are a
> large number of cores, this may be too much overhead.
> 
> Instead of checking the fpq processing list, check ent_w_req_queue,
> ent_in_userspace, and ent_commit_queue.
> 
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
>  fs/fuse/dev.c        |  2 +-
>  fs/fuse/dev_uring.c  | 23 ++++++++++++++++++++---
>  fs/fuse/fuse_dev_i.h |  1 -
>  3 files changed, 21 insertions(+), 5 deletions(-)
> 
> diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
> index 3c03aac480a4..80a11ef4b69a 100644
> --- a/fs/fuse/dev.c
> +++ b/fs/fuse/dev.c
> @@ -45,7 +45,7 @@ bool fuse_request_expired(struct fuse_conn *fc, struct list_head *list)
>  	return time_is_before_jiffies(req->create_time + fc->timeout.req_timeout);
>  }
>  
> -bool fuse_fpq_processing_expired(struct fuse_conn *fc, struct list_head *processing)
> +static bool fuse_fpq_processing_expired(struct fuse_conn *fc, struct list_head *processing)
>  {
>  	int i;
>  
> diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c
> index 5c9b5a5fb7f7..dfa6c5337bbf 100644
> --- a/fs/fuse/dev_uring.c
> +++ b/fs/fuse/dev_uring.c
> @@ -90,6 +90,7 @@ static void fuse_uring_req_end(struct fuse_ring_ent *ent, int error)
>  		fuse_uring_flush_bg(queue);
>  		spin_unlock(&fc->bg_lock);
>  	}
> +	ent->fuse_req = NULL;
>  
>  	spin_unlock(&queue->lock);
>  
> @@ -97,8 +98,7 @@ static void fuse_uring_req_end(struct fuse_ring_ent *ent, int error)
>  		req->out.h.error = error;
>  
>  	clear_bit(FR_SENT, &req->flags);
> -	fuse_request_end(ent->fuse_req);
> -	ent->fuse_req = NULL;
> +	fuse_request_end(req);
>  }
>  

Oh, this is actually a fix, it should be always set with the lock being
held.



>  /* Abort all list queued request on the given ring queue */
> @@ -140,6 +140,21 @@ void fuse_uring_abort_end_requests(struct fuse_ring *ring)
>  	}
>  }
>  
> +static bool ent_list_request_expired(struct fuse_conn *fc, struct list_head *list)
> +{
> +	struct fuse_ring_ent *ent;
> +	struct fuse_req *req;
> +
> +	list_for_each_entry(ent, list, list) {
> +		req = ent->fuse_req;
> +		if (req)
> +			return time_is_before_jiffies(req->create_time +
> +						      fc->timeout.req_timeout);
> +	}
> +
> +	return false;
> +}

Hmm, would only need to check head? Oh I see it, we need to use
list_move_tail().  


> +
>  bool fuse_uring_request_expired(struct fuse_conn *fc)
>  {
>  	struct fuse_ring *ring = fc->ring;
> @@ -157,7 +172,9 @@ bool fuse_uring_request_expired(struct fuse_conn *fc)
>  		spin_lock(&queue->lock);
>  		if (fuse_request_expired(fc, &queue->fuse_req_queue) ||
>  		    fuse_request_expired(fc, &queue->fuse_req_bg_queue) ||
> -		    fuse_fpq_processing_expired(fc, queue->fpq.processing)) {
> +		    ent_list_request_expired(fc, &queue->ent_w_req_queue) ||
> +		    ent_list_request_expired(fc, &queue->ent_in_userspace) ||
> +		    ent_list_request_expired(fc, &queue->ent_commit_queue)) {
>  			spin_unlock(&queue->lock);
>  			return true;
>  		}
> diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h
> index 3c4ae4d52b6f..19c29c6000a7 100644
> --- a/fs/fuse/fuse_dev_i.h
> +++ b/fs/fuse/fuse_dev_i.h
> @@ -63,7 +63,6 @@ void fuse_dev_queue_forget(struct fuse_iqueue *fiq,
>  void fuse_dev_queue_interrupt(struct fuse_iqueue *fiq, struct fuse_req *req);
>  
>  bool fuse_request_expired(struct fuse_conn *fc, struct list_head *list);
> -bool fuse_fpq_processing_expired(struct fuse_conn *fc, struct list_head *processing);
>  
>  #endif
>
Bernd Schubert Jan. 24, 2025, 6:22 p.m. UTC | #2
Hi Joanne,

On 1/24/25 12:30, Bernd Schubert wrote:
> Hmm, would only need to check head? Oh I see it, we need to use
> list_move_tail().  


how about the attached updated patch, which uses
list_first_entry_or_null()? It also changes from list_move()
to list_move_tail() so that oldest entry is always on top.
I didn't give it any testing, though.

This is on top of the other fuse-uring updates I had sent
out about an hour ago.


Here is the corresponding branch
https://github.com/bsbernd/linux/tree/optimize-fuse-uring-req-timeouts


Thanks,
Bernd
Joanne Koong Jan. 24, 2025, 10:05 p.m. UTC | #3
On Fri, Jan 24, 2025 at 10:22 AM Bernd Schubert
<bernd.schubert@fastmail.fm> wrote:
>
> Hi Joanne,
>
> On 1/24/25 12:30, Bernd Schubert wrote:
> > Hmm, would only need to check head? Oh I see it, we need to use
> > list_move_tail().
>
>
> how about the attached updated patch, which uses
> list_first_entry_or_null()? It also changes from list_move()
> to list_move_tail() so that oldest entry is always on top.
> I didn't give it any testing, though.

Woah that's cool, I didn't know you could send attachments over the
mailing list.
Ah I didn't realize list_move doesn't already by default add to the
tail of the list - thanks for catching that, yes those should be
list_move_tail() then.

In t he attached patch, I think we still need the original
ent_list_request_expired() logic:

static bool ent_list_request_expired(struct fuse_conn *fc, struct
list_head *list)
{
    struct fuse_ring_ent *ent;
    struct fuse_req *req;

    list_for_each_entry(ent, list, list) {
    req = ent->fuse_req;
    if (req)
        return time_is_before_jiffies(req->create_time +
                    fc->timeout.req_timeout);
    }

    return false;
}

and we can't assume req is non-NULL. For entries that have been
committed, their ->req is set to NULL but they are still on the
ent_commit_queue.



Thanks,
Joanne

>
> This is on top of the other fuse-uring updates I had sent
> out about an hour ago.
>
>
> Here is the corresponding branch
> https://github.com/bsbernd/linux/tree/optimize-fuse-uring-req-timeouts
>
>
> Thanks,
> Bernd
Bernd Schubert Jan. 24, 2025, 10:58 p.m. UTC | #4
On 1/24/25 23:05, Joanne Koong wrote:
> On Fri, Jan 24, 2025 at 10:22 AM Bernd Schubert
> <bernd.schubert@fastmail.fm> wrote:
>>
>> Hi Joanne,
>>
>> On 1/24/25 12:30, Bernd Schubert wrote:
>>> Hmm, would only need to check head? Oh I see it, we need to use
>>> list_move_tail().
>>
>>
>> how about the attached updated patch, which uses
>> list_first_entry_or_null()? It also changes from list_move()
>> to list_move_tail() so that oldest entry is always on top.
>> I didn't give it any testing, though.
> 
> Woah that's cool, I didn't know you could send attachments over the
> mailing list.
> Ah I didn't realize list_move doesn't already by default add to the
> tail of the list - thanks for catching that, yes those should be
> list_move_tail() then.
> 
> In t he attached patch, I think we still need the original
> ent_list_request_expired() logic:
> 
> static bool ent_list_request_expired(struct fuse_conn *fc, struct
> list_head *list)
> {
>     struct fuse_ring_ent *ent;
>     struct fuse_req *req;
> 
>     list_for_each_entry(ent, list, list) {
>     req = ent->fuse_req;
>     if (req)
>         return time_is_before_jiffies(req->create_time +
>                     fc->timeout.req_timeout);
>     }
> 
>     return false;
> }

Could you explain why? That would be super expensive if lists
have many entries? Due to fg and bg queues it might not be
perfectly ordered, but that it actually also true for
fuse_req_queue and also without io-uring. Server might process
requests in different order, so ent_commit_queue might also not
be perfectly sorted. But then I'm not even sure if we need to
process that queue, as it has entries that are already processed - at
best that would catch fuse client/kernel bugs.
Though, if there is some kind if req processing issue, either
everything times out, or the head will eventually get the older
requests. So I don't understand why would need to go over all entries.

> 
> and we can't assume req is non-NULL. For entries that have been
> committed, their ->req is set to NULL but they are still on the
> ent_commit_queue.

Yeah sorry, right, though I wonder if we can just avoid checking
that queue.


Thanks,
Bernd
Joanne Koong Jan. 24, 2025, 11:31 p.m. UTC | #5
On Fri, Jan 24, 2025 at 2:58 PM Bernd Schubert
<bernd.schubert@fastmail.fm> wrote:
>
>
>
> On 1/24/25 23:05, Joanne Koong wrote:
> > On Fri, Jan 24, 2025 at 10:22 AM Bernd Schubert
> > <bernd.schubert@fastmail.fm> wrote:
> >>
> >> Hi Joanne,
> >>
> >> On 1/24/25 12:30, Bernd Schubert wrote:
> >>> Hmm, would only need to check head? Oh I see it, we need to use
> >>> list_move_tail().
> >>
> >>
> >> how about the attached updated patch, which uses
> >> list_first_entry_or_null()? It also changes from list_move()
> >> to list_move_tail() so that oldest entry is always on top.
> >> I didn't give it any testing, though.
> >
> > Woah that's cool, I didn't know you could send attachments over the
> > mailing list.
> > Ah I didn't realize list_move doesn't already by default add to the
> > tail of the list - thanks for catching that, yes those should be
> > list_move_tail() then.
> >
> > In t he attached patch, I think we still need the original
> > ent_list_request_expired() logic:
> >
> > static bool ent_list_request_expired(struct fuse_conn *fc, struct
> > list_head *list)
> > {
> >     struct fuse_ring_ent *ent;
> >     struct fuse_req *req;
> >
> >     list_for_each_entry(ent, list, list) {
> >     req = ent->fuse_req;
> >     if (req)
> >         return time_is_before_jiffies(req->create_time +
> >                     fc->timeout.req_timeout);
> >     }
> >
> >     return false;
> > }
>
> Could you explain why? That would be super expensive if lists
> have many entries? Due to fg and bg queues it might not be
> perfectly ordered, but that it actually also true for
> fuse_req_queue and also without io-uring. Server might process
> requests in different order, so ent_commit_queue might also not
> be perfectly sorted. But then I'm not even sure if we need to
> process that queue, as it has entries that are already processed - at
> best that would catch fuse client/kernel bugs.
> Though, if there is some kind if req processing issue, either
> everything times out, or the head will eventually get the older
> requests. So I don't understand why would need to go over all entries.
>

I don't think this in most cases goes over all entries. For
ent_w_req_queue and ent_in_userspace queues, I think this is
equivalent to just checking the head of the list since every entry on
it will have a non-NULL request attached. But I think we need this
"list_for_each_entry() { ... if (req) ... }" logic in order to
properly handle the ent_commit_queue cases where an entry is on that
queue but has a NULL request (eg the condition where
fuse_uring_commit() has finished being called on the entry but a new
request hasn't yet been attached to that entry).

I think another option is if we move the fuse_uring_ent_avail() call
from fuse_uring_next_fuse_req() to be in fuse_uring_req_end() within
the scope of the queue->lock instead, then that ensures all entries on
the committed queue have valid requests.


> >
> > and we can't assume req is non-NULL. For entries that have been

sorry, this should have been "because we can't assume req is
non-NULL", not "and".

> > committed, their ->req is set to NULL but they are still on the
> > ent_commit_queue.
>
> Yeah sorry, right, though I wonder if we can just avoid checking
> that queue.
>

I like the idea of skipping the ent_commit_queue! We pretty much know
the request is being responded to right now if it's being committed,
so even if it has technically timed out, we can skip aborting it.
Timeouts are kind of fuzzy right now anyways given the
FUSE_TIMEOUT_TIMER_FREQ periodic check, so I don't see why this would
be a problem.


Thanks,
Joanne
>
> Thanks,
> Bernd
Joanne Koong Jan. 24, 2025, 11:33 p.m. UTC | #6
On Fri, Jan 24, 2025 at 3:31 PM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> On Fri, Jan 24, 2025 at 2:58 PM Bernd Schubert
> <bernd.schubert@fastmail.fm> wrote:
> >
> >
> >
> > On 1/24/25 23:05, Joanne Koong wrote:
> > > On Fri, Jan 24, 2025 at 10:22 AM Bernd Schubert
> > > <bernd.schubert@fastmail.fm> wrote:
> > >>
> > >> Hi Joanne,
> > >>
> > >> On 1/24/25 12:30, Bernd Schubert wrote:
> > >>> Hmm, would only need to check head? Oh I see it, we need to use
> > >>> list_move_tail().
> > >>
> > >>
> > >> how about the attached updated patch, which uses
> > >> list_first_entry_or_null()? It also changes from list_move()
> > >> to list_move_tail() so that oldest entry is always on top.
> > >> I didn't give it any testing, though.

I think this attached patch looks good, except with the
"ent_list_request_expired(fc, &queue->ent_commit_queue))" line in it
removed.

Thanks,
Joanne

> > >
> > > Woah that's cool, I didn't know you could send attachments over the
> > > mailing list.
> > > Ah I didn't realize list_move doesn't already by default add to the
> > > tail of the list - thanks for catching that, yes those should be
> > > list_move_tail() then.
> > >
> > > In t he attached patch, I think we still need the original
> > > ent_list_request_expired() logic:
> > >
> > > static bool ent_list_request_expired(struct fuse_conn *fc, struct
> > > list_head *list)
> > > {
> > >     struct fuse_ring_ent *ent;
> > >     struct fuse_req *req;
> > >
> > >     list_for_each_entry(ent, list, list) {
> > >     req = ent->fuse_req;
> > >     if (req)
> > >         return time_is_before_jiffies(req->create_time +
> > >                     fc->timeout.req_timeout);
> > >     }
> > >
> > >     return false;
> > > }
> >
> > Could you explain why? That would be super expensive if lists
> > have many entries? Due to fg and bg queues it might not be
> > perfectly ordered, but that it actually also true for
> > fuse_req_queue and also without io-uring. Server might process
> > requests in different order, so ent_commit_queue might also not
> > be perfectly sorted. But then I'm not even sure if we need to
> > process that queue, as it has entries that are already processed - at
> > best that would catch fuse client/kernel bugs.
> > Though, if there is some kind if req processing issue, either
> > everything times out, or the head will eventually get the older
> > requests. So I don't understand why would need to go over all entries.
> >
>
> I don't think this in most cases goes over all entries. For
> ent_w_req_queue and ent_in_userspace queues, I think this is
> equivalent to just checking the head of the list since every entry on
> it will have a non-NULL request attached. But I think we need this
> "list_for_each_entry() { ... if (req) ... }" logic in order to
> properly handle the ent_commit_queue cases where an entry is on that
> queue but has a NULL request (eg the condition where
> fuse_uring_commit() has finished being called on the entry but a new
> request hasn't yet been attached to that entry).
>
> I think another option is if we move the fuse_uring_ent_avail() call
> from fuse_uring_next_fuse_req() to be in fuse_uring_req_end() within
> the scope of the queue->lock instead, then that ensures all entries on
> the committed queue have valid requests.
>
>
> > >
> > > and we can't assume req is non-NULL. For entries that have been
>
> sorry, this should have been "because we can't assume req is
> non-NULL", not "and".
>
> > > committed, their ->req is set to NULL but they are still on the
> > > ent_commit_queue.
> >
> > Yeah sorry, right, though I wonder if we can just avoid checking
> > that queue.
> >
>
> I like the idea of skipping the ent_commit_queue! We pretty much know
> the request is being responded to right now if it's being committed,
> so even if it has technically timed out, we can skip aborting it.
> Timeouts are kind of fuzzy right now anyways given the
> FUSE_TIMEOUT_TIMER_FREQ periodic check, so I don't see why this would
> be a problem.
>
>
> Thanks,
> Joanne
> >
> > Thanks,
> > Bernd
diff mbox series

Patch

diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
index 3c03aac480a4..80a11ef4b69a 100644
--- a/fs/fuse/dev.c
+++ b/fs/fuse/dev.c
@@ -45,7 +45,7 @@  bool fuse_request_expired(struct fuse_conn *fc, struct list_head *list)
 	return time_is_before_jiffies(req->create_time + fc->timeout.req_timeout);
 }
 
-bool fuse_fpq_processing_expired(struct fuse_conn *fc, struct list_head *processing)
+static bool fuse_fpq_processing_expired(struct fuse_conn *fc, struct list_head *processing)
 {
 	int i;
 
diff --git a/fs/fuse/dev_uring.c b/fs/fuse/dev_uring.c
index 5c9b5a5fb7f7..dfa6c5337bbf 100644
--- a/fs/fuse/dev_uring.c
+++ b/fs/fuse/dev_uring.c
@@ -90,6 +90,7 @@  static void fuse_uring_req_end(struct fuse_ring_ent *ent, int error)
 		fuse_uring_flush_bg(queue);
 		spin_unlock(&fc->bg_lock);
 	}
+	ent->fuse_req = NULL;
 
 	spin_unlock(&queue->lock);
 
@@ -97,8 +98,7 @@  static void fuse_uring_req_end(struct fuse_ring_ent *ent, int error)
 		req->out.h.error = error;
 
 	clear_bit(FR_SENT, &req->flags);
-	fuse_request_end(ent->fuse_req);
-	ent->fuse_req = NULL;
+	fuse_request_end(req);
 }
 
 /* Abort all list queued request on the given ring queue */
@@ -140,6 +140,21 @@  void fuse_uring_abort_end_requests(struct fuse_ring *ring)
 	}
 }
 
+static bool ent_list_request_expired(struct fuse_conn *fc, struct list_head *list)
+{
+	struct fuse_ring_ent *ent;
+	struct fuse_req *req;
+
+	list_for_each_entry(ent, list, list) {
+		req = ent->fuse_req;
+		if (req)
+			return time_is_before_jiffies(req->create_time +
+						      fc->timeout.req_timeout);
+	}
+
+	return false;
+}
+
 bool fuse_uring_request_expired(struct fuse_conn *fc)
 {
 	struct fuse_ring *ring = fc->ring;
@@ -157,7 +172,9 @@  bool fuse_uring_request_expired(struct fuse_conn *fc)
 		spin_lock(&queue->lock);
 		if (fuse_request_expired(fc, &queue->fuse_req_queue) ||
 		    fuse_request_expired(fc, &queue->fuse_req_bg_queue) ||
-		    fuse_fpq_processing_expired(fc, queue->fpq.processing)) {
+		    ent_list_request_expired(fc, &queue->ent_w_req_queue) ||
+		    ent_list_request_expired(fc, &queue->ent_in_userspace) ||
+		    ent_list_request_expired(fc, &queue->ent_commit_queue)) {
 			spin_unlock(&queue->lock);
 			return true;
 		}
diff --git a/fs/fuse/fuse_dev_i.h b/fs/fuse/fuse_dev_i.h
index 3c4ae4d52b6f..19c29c6000a7 100644
--- a/fs/fuse/fuse_dev_i.h
+++ b/fs/fuse/fuse_dev_i.h
@@ -63,7 +63,6 @@  void fuse_dev_queue_forget(struct fuse_iqueue *fiq,
 void fuse_dev_queue_interrupt(struct fuse_iqueue *fiq, struct fuse_req *req);
 
 bool fuse_request_expired(struct fuse_conn *fc, struct list_head *list);
-bool fuse_fpq_processing_expired(struct fuse_conn *fc, struct list_head *processing);
 
 #endif