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

CA-388564: move qemu-dm to vm.slice #6150

Merged
merged 1 commit into from
Dec 4, 2024

Conversation

edwintorok
Copy link
Contributor

This is a single line change to move qemu to the proper cgroup.

Qemu moves itself to the root cgroup, which makes it a sibling of control.slice. All toolstack processes are in control.slice, and even though control.slice gets 10x share of CPU, if we have 1000 qemu processes there is still a big imbalance (toolstack gets 10/1010 share of the CPU)

Qemu-wrapper already has the ability to move the process to another cgroup, so use it. tapdisk and qemu-datapath are already in vm.slice, so move qemu-dm there too.

Checked that qemu ends up in the correct slice:

cat /proc/$(pgrep qemu-dm\*|head -n1)/cgroup
8:memory:/vm.slice
7:blkio:/vm.slice
6:net_cls,net_prio:/
5:freezer:/
4:cpu,cpuacct:/vm.slice
3:cpuset:/
2:devices:/
1:name=systemd:/vm.slice/[email protected]

Tested with a script that control.slice and vm.slice are indeed properly limiting CPU usage, and control.slice gets the expected share of CPU even when vm.slice has a lot more processes:

#!/bin/bash
set -eu

# $$ is the pid of the main shell
# Need to use $BASHPID to get pid of subshell

for SLICE in system.slice control.slice vm.slice; do
	for i in $(seq 1 16); do
		(echo $BASHPID >/sys/fs/cgroup/cpu,cpuacct/${SLICE}/tasks; while true; do echo -n; done)&
	done
done

for i in $(seq 17 1000); do
	(echo $BASHPID >/sys/fs/cgroup/cpu,cpuacct/vm.slice/tasks; while true; do echo -n; done)&
done
329178 root      20   0  113216    216     16 R  92.4  0.0   0:07.50 bash
 329171 root      20   0  113216    216     16 R  84.4  0.0   0:07.05 bash
 329177 root      20   0  113216    216     16 R  84.4  0.0   0:06.97 bash
 329170 root      20   0  113216    216     16 R  84.1  0.0   0:06.65 bash
 329180 root      20   0  113216    216     16 R  84.1  0.0   0:06.94 bash
 329183 root      20   0  113216    220     16 R  81.8  0.0   0:06.93 bash
 329174 root      20   0  113216    216     16 R  80.6  0.0   0:06.90 bash
 329181 root      20   0  113216    216     16 R  79.9  0.0   0:07.13 bash
 329179 root      20   0  113216    216     16 R  79.3  0.0   0:06.78 bash
 329184 root      20   0  113216    220     16 R  79.3  0.0   0:06.55 bash
 329175 root      20   0  113216    216     16 R  79.0  0.0   0:06.87 bash
 329172 root      20   0  113216    216     16 R  78.7  0.0   0:07.29 bash
 329173 root      20   0  113216    216     16 R  78.7  0.0   0:06.86 bash
 329169 root      20   0  113216    216     16 R  77.4  0.0   0:07.04 bash
 329182 root      20   0  113216    220     16 R  76.4  0.0   0:06.75 bash
 329176 root      20   0  113216    216     16 R  76.1  0.0   0:06.76 bash
 329152 root      20   0  113216    212     16 R  11.1  0.0   0:00.83 bash
 329155 root      20   0  113216    212     16 R   9.9  0.0   0:00.87 bash
 329160 root      20   0  113216    212     16 R   8.6  0.0   0:00.70 bash
 329161 root      20   0  113216    212     16 R   8.3  0.0   0:00.71 bash
 329156 root      20   0  113216    212     16 R   8.0  0.0   0:00.66 bash
 329158 root      20   0  113216    212     16 R   8.0  0.0   0:00.66 bash
 329163 root      20   0  113216    212     16 R   8.0  0.0   0:00.88 bash
 329154 root      20   0  113216    212     16 R   7.6  0.0   0:00.66 bash
 329159 root      20   0  113216    212     16 R   7.6  0.0   0:00.67 bash
 329164 root      20   0  113216    212     16 R   7.6  0.0   0:00.68 bash
 329165 root      20   0  113216    212     16 R   7.6  0.0   0:00.79 bash
 329166 root      20   0  113216    216     16 R   7.6  0.0   0:00.65 bash
 329153 root      20   0  113216    212     16 R   7.3  0.0   0:00.67 bash
 329157 root      20   0  113216    212     16 R   7.3  0.0   0:00.73 bash
 329162 root      20   0  113216    212     16 R   7.3  0.0   0:00.64 bash
 329167 root      20   0  113216    216     16 R   7.3  0.0   0:00.64 bash

And verifying some of the high CPU usage bash processes vs some of the low ones: the high ones are in control.slice, the low ones are in other slices.

Qemu moves itself to the root cgroup, which makes it a sibling of control.slice.
All toolstack processes are in control.slice, and even though control.slice gets 10x share of CPU,
if we have 1000 qemu processes there is still a big imbalance (toolstack gets 10/1010 share of the CPU)

Qemu-wrapper already has the ability to move the process to another cgroup, so use it.
tapdisk and qemu-datapath are already in vm.slice, so move qemu-dm there too.

Signed-off-by: Edwin Török <[email protected]>
@edwintorok edwintorok merged commit 59da2e0 into xapi-project:feature/perf Dec 4, 2024
15 checks passed
@liulinC
Copy link
Collaborator

liulinC commented Dec 5, 2024

cgroupV2 on XS9 has different view, (Currently V1 does not work well on XS9 neither)
what is the result on XS9?

edwintorok added a commit that referenced this pull request Dec 10, 2024
And solve conflicts.

The conflict resolution can be reviewed locally with this command if you
have a new enough version of `git`:
```
git log --remerge-diff -1 81146d223d3d5cd449105ecef713cfd57e2c1853
```

The conflicts are mostly due to:
* with_tracing being removed from the Http library
* the xapi_periodic_scheduler being moved to xapi-stdext-threads
* a slightly different version of the concurrent PR being merged between
the 2 branches (`vm` instead of `vm'`)

For convenience here is the output of that command:
```diff
commit 81146d223d3d5cd449105ecef713cfd57e2c1853
Merge: 59da2e0 d8baca7
Author: Edwin Török <[email protected]>
Date:   Tue Dec 10 13:43:23 2024 +0000

    Merge master into feature/perf

    Fix conflicts in http.ml (with_tracing got moved),
    and periodic scheduler (got moved to xapi-stdext-threads).

    Signed-off-by: Edwin Török <[email protected]>

diff --git a/ocaml/libs/http-lib/http.ml b/ocaml/libs/http-lib/http.ml
remerge CONFLICT (content): Merge conflict in ocaml/libs/http-lib/http.ml
index ca635e2ba0..c979e1f7d9 100644
--- a/ocaml/libs/http-lib/http.ml
+++ b/ocaml/libs/http-lib/http.ml
@@ -132,16 +132,8 @@ module Hdr = struct

   let location = "location"

-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
   let originator = "originator"

-  let traceparent = "traceparent"
-
-||||||| 77dd474
-  let traceparent = "traceparent"
-
-=======
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
   let hsts = "strict-transport-security"
 end

@@ -684,7 +676,6 @@ module Request = struct
     let headers, body = to_headers_and_body x in
     let frame_header = if x.frame then make_frame_header headers else "" in
     frame_header ^ headers ^ body
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))

   let with_originator_of req f =
     Option.iter
@@ -693,55 +684,6 @@ module Request = struct
         f originator
       )
       req
-
-  let traceparent_of req =
-    let open Tracing in
-    let ( let* ) = Option.bind in
-    let* traceparent = req.traceparent in
-    let* span_context = SpanContext.of_traceparent traceparent in
-    let span = Tracer.span_of_span_context span_context req.uri in
-    Some span
-
-  let with_tracing ?attributes ~name req f =
-    let open Tracing in
-    let parent = traceparent_of req in
-    with_child_trace ?attributes parent ~name (fun (span : Span.t option) ->
-        match span with
-        | Some span ->
-            let traceparent =
-              Some (span |> Span.get_context |> SpanContext.to_traceparent)
-            in
-            let req = {req with traceparent} in
-            f req
-        | None ->
-            f req
-    )
-||||||| 77dd474
-
-  let traceparent_of req =
-    let open Tracing in
-    let ( let* ) = Option.bind in
-    let* traceparent = req.traceparent in
-    let* span_context = SpanContext.of_traceparent traceparent in
-    let span = Tracer.span_of_span_context span_context req.uri in
-    Some span
-
-  let with_tracing ?attributes ~name req f =
-    let open Tracing in
-    let parent = traceparent_of req in
-    with_child_trace ?attributes parent ~name (fun (span : Span.t option) ->
-        match span with
-        | Some span ->
-            let traceparent =
-              Some (span |> Span.get_context |> SpanContext.to_traceparent)
-            in
-            let req = {req with traceparent} in
-            f req
-        | None ->
-            f req
-    )
-=======
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
 end

 module Response = struct
diff --git a/ocaml/libs/http-lib/http.mli b/ocaml/libs/http-lib/http.mli
remerge CONFLICT (content): Merge conflict in ocaml/libs/http-lib/http.mli
index e77a9ebd5a..114ddbc4f4 100644
--- a/ocaml/libs/http-lib/http.mli
+++ b/ocaml/libs/http-lib/http.mli
@@ -126,22 +126,8 @@ module Request : sig

   val to_wire_string : t -> string
   (** [to_wire_string t] returns a string which could be sent to a server *)
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))

   val with_originator_of : t option -> (string option -> unit) -> unit
-
-  val traceparent_of : t -> Tracing.Span.t option
-
-  val with_tracing :
-    ?attributes:(string * string) list -> name:string -> t -> (t -> 'a) -> 'a
-||||||| 77dd474
-
-  val traceparent_of : t -> Tracing.Span.t option
-
-  val with_tracing :
-    ?attributes:(string * string) list -> name:string -> t -> (t -> 'a) -> 'a
-=======
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
 end

 (** Parsed form of the HTTP response *)
diff --git a/ocaml/tests/dune b/ocaml/tests/dune
index b51bbca8b8..ce8fe96c19 100644
--- a/ocaml/tests/dune
+++ b/ocaml/tests/dune
@@ -118,6 +118,7 @@
     xapi-types
     xapi-stdext-date
     xapi-stdext-threads
+    xapi-stdext-threads.scheduler
     xapi-stdext-unix
     xml-light2
     yojson
diff --git a/ocaml/tests/test_event.ml b/ocaml/tests/test_event.ml
index d36dba90ef..821bb3bb52 100644
--- a/ocaml/tests/test_event.ml
+++ b/ocaml/tests/test_event.ml
@@ -287,7 +287,7 @@ let test_short_oneshot () =
     started := true ;
     Condition.broadcast cond ;
     Mutex.unlock m ;
-    Xapi_periodic_scheduler.loop ()
+    Xapi_stdext_threads_scheduler.Scheduler.loop ()
   in
   ignore (Thread.create scheduler ()) ;
   (* ensure scheduler sees an empty queue , by waiting for it to start *)
@@ -303,8 +303,8 @@ let test_short_oneshot () =
   let fired = Atomic.make false in
   let fire () = Atomic.set fired true in
   let task = "test_oneshot" in
-  Xapi_periodic_scheduler.add_to_queue task Xapi_periodic_scheduler.OneShot 1.
-    fire ;
+  Xapi_stdext_threads_scheduler.Scheduler.add_to_queue task
+    Xapi_stdext_threads_scheduler.Scheduler.OneShot 1. fire ;
   Thread.delay 2. ;
   assert (Atomic.get fired)

diff --git a/ocaml/xapi-storage-script/main.ml b/ocaml/xapi-storage-script/main.ml
remerge CONFLICT (content): Merge conflict in ocaml/xapi-storage-script/main.ml
index 1ec4d9dcb1..cba7ec89d5 100644
--- a/ocaml/xapi-storage-script/main.ml
+++ b/ocaml/xapi-storage-script/main.ml
@@ -66,7 +66,6 @@ let backend_backtrace_error name args backtrace =
 let missing_uri () =
   backend_error "MISSING_URI" ["Please include a URI in the device-config"]

-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
 (** return a unique 'domain' string for Dom0, so that we can plug disks
   multiple times (e.g. for copy).

@@ -84,26 +83,6 @@ let domain_of ~dp ~vm =
   | _ ->
       vm

-||||||| 77dd474
-=======
-(** return a unique 'domain' string for Dom0, so that we can plug disks
-  multiple times (e.g. for copy).
-
-  XAPI should give us a unique 'dp' (datapath) string, e.g. a UUID for storage migration,
-  or vbd/domid/device.
-  For regular guests keep the domain as passed by XAPI (an integer).
- *)
-let domain_of ~dp ~vm' =
-  let vm = Storage_interface.Vm.string_of vm' in
-  match vm with
-  | "0" ->
-      (* SM tries to use this in filesystem paths, so cannot have /,
-         and systemd might be a bit unhappy with - *)
-      "u0-" ^ dp |> String.map (function '/' | '-' -> '_' | c -> c)
-  | _ ->
-      vm
-
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
 (** Functions to wrap calls to the above client modules and convert their
     exceptions and errors into SMAPIv2 errors of type
     [Storage_interface.Exception.exnty]. The above client modules should only
@@ -1476,21 +1455,9 @@ let bind ~volume_script_dir =
     |> wrap
   in
   S.VDI.introduce vdi_introduce_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
   let vdi_attach3_impl dbg dp sr vdi' vm _readwrite =
-||||||| 77dd474
-  let vdi_attach3_impl dbg _dp sr vdi' vm' _readwrite =
-=======
-  let vdi_attach3_impl dbg dp sr vdi' vm' _readwrite =
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
     (let vdi = Storage_interface.Vdi.string_of vdi' in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
      let domain = domain_of ~dp ~vm in
-||||||| 77dd474
-     let domain = Storage_interface.Vm.string_of vm' in
-=======
-     let domain = domain_of ~dp ~vm' in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
      vdi_attach_common dbg sr vdi domain >>>= fun response ->
      let convert_implementation = function
        | Xapi_storage.Data.XenDisk {params; extra; backend_type} ->
@@ -1512,21 +1479,9 @@ let bind ~volume_script_dir =
     |> wrap
   in
   S.VDI.attach3 vdi_attach3_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
   let vdi_activate_common dbg dp sr vdi' vm readonly =
-||||||| 77dd474
-  let vdi_activate_common dbg sr vdi' vm' readonly =
-=======
-  let vdi_activate_common dbg dp sr vdi' vm' readonly =
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
     (let vdi = Storage_interface.Vdi.string_of vdi' in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
      let domain = domain_of ~dp ~vm in
-||||||| 77dd474
-     let domain = Storage_interface.Vm.string_of vm' in
-=======
-     let domain = domain_of ~dp ~vm' in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
      Attached_SRs.find sr >>>= fun sr ->
      (* Discover the URIs using Volume.stat *)
      stat ~dbg ~sr ~vdi >>>= fun response ->
@@ -1551,45 +1506,17 @@ let bind ~volume_script_dir =
     )
     |> wrap
   in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
   let vdi_activate3_impl dbg dp sr vdi' vm =
     vdi_activate_common dbg dp sr vdi' vm false
-||||||| 77dd474
-  let vdi_activate3_impl dbg _dp sr vdi' vm' =
-    vdi_activate_common dbg sr vdi' vm' false
-=======
-  let vdi_activate3_impl dbg dp sr vdi' vm' =
-    vdi_activate_common dbg dp sr vdi' vm' false
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
   in
   S.VDI.activate3 vdi_activate3_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
   let vdi_activate_readonly_impl dbg dp sr vdi' vm =
     vdi_activate_common dbg dp sr vdi' vm true
-||||||| 77dd474
-  let vdi_activate_readonly_impl dbg _dp sr vdi' vm' =
-    vdi_activate_common dbg sr vdi' vm' true
-=======
-  let vdi_activate_readonly_impl dbg dp sr vdi' vm' =
-    vdi_activate_common dbg dp sr vdi' vm' true
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
   in
   S.VDI.activate_readonly vdi_activate_readonly_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
   let vdi_deactivate_impl dbg dp sr vdi' vm =
-||||||| 77dd474
-  let vdi_deactivate_impl dbg _dp sr vdi' vm' =
-=======
-  let vdi_deactivate_impl dbg dp sr vdi' vm' =
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
     (let vdi = Storage_interface.Vdi.string_of vdi' in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
      let domain = domain_of ~dp ~vm in
-||||||| 77dd474
-     let domain = Storage_interface.Vm.string_of vm' in
-=======
-     let domain = domain_of ~dp ~vm' in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
      Attached_SRs.find sr >>>= fun sr ->
      (* Discover the URIs using Volume.stat *)
      stat ~dbg ~sr ~vdi >>>= fun response ->
@@ -1610,21 +1537,9 @@ let bind ~volume_script_dir =
     |> wrap
   in
   S.VDI.deactivate vdi_deactivate_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
   let vdi_detach_impl dbg dp sr vdi' vm =
-||||||| 77dd474
-  let vdi_detach_impl dbg _dp sr vdi' vm' =
-=======
-  let vdi_detach_impl dbg dp sr vdi' vm' =
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
     (let vdi = Storage_interface.Vdi.string_of vdi' in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
      let domain = domain_of ~dp ~vm in
-||||||| 77dd474
-     let domain = Storage_interface.Vm.string_of vm' in
-=======
-     let domain = domain_of ~dp ~vm' in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
      Attached_SRs.find sr >>>= fun sr ->
      (* Discover the URIs using Volume.stat *)
      stat ~dbg ~sr ~vdi >>>= fun response ->
@@ -1732,21 +1647,9 @@ let bind ~volume_script_dir =
   S.VDI.epoch_end vdi_epoch_end_impl ;
   let vdi_set_persistent_impl _dbg _sr _vdi _persistent = return () |> wrap in
   S.VDI.set_persistent vdi_set_persistent_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
   let dp_destroy2 dbg dp sr vdi' vm _allow_leak =
-||||||| 77dd474
-  let dp_destroy2 dbg _dp sr vdi' vm' _allow_leak =
-=======
-  let dp_destroy2 dbg dp sr vdi' vm' _allow_leak =
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
     (let vdi = Storage_interface.Vdi.string_of vdi' in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
      let domain = domain_of ~dp ~vm in
-||||||| 77dd474
-     let domain = Storage_interface.Vm.string_of vm' in
-=======
-     let domain = domain_of ~dp ~vm' in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
      Attached_SRs.find sr >>>= fun sr ->
      (* Discover the URIs using Volume.stat *)
      stat ~dbg ~sr ~vdi >>>= fun response ->
@@ -1888,17 +1791,7 @@ let rec diff a b =
   | a :: aa ->
       if List.mem a b then diff aa b else a :: diff aa b

-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
 let concurrent = ref true
-||||||| 77dd474
-(* default false due to bugs in SMAPIv3 plugins,
-   once they are fixed this should be set to true *)
-let concurrent = ref false
-=======
-(* default false due to bugs in SMAPIv3 plugins,
-   once they are fixed this should be set to true *)
-let concurrent = ref true
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))

 type reload = All | Files of string list | Nothing

diff --git a/ocaml/xapi/helpers.ml b/ocaml/xapi/helpers.ml
remerge CONFLICT (content): Merge conflict in ocaml/xapi/helpers.ml
index ca57afb8d8..1175b6aa03 100644
--- a/ocaml/xapi/helpers.ml
+++ b/ocaml/xapi/helpers.ml
@@ -410,20 +410,14 @@ let make_rpc ~__context rpc : Rpc.response =
   let subtask_of = Ref.string_of (Context.get_task_id __context) in
   let open Xmlrpc_client in
   let tracing = Context.set_client_span __context in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (#6150))
   let dorpc, path =
     if !Xapi_globs.use_xmlrpc then
       (XMLRPC_protocol.rpc, "/")
     else
       (JSONRPC_protocol.rpc, "/jsonrpc")
   in
-  let http = xmlrpc ~subtask_of ~version:"1.1" path ~tracing in
-||||||| 77dd474
-  let http = xmlrpc ~subtask_of ~version:"1.1" "/" ~tracing in
-=======
-  let http = xmlrpc ~subtask_of ~version:"1.1" "/" in
+  let http = xmlrpc ~subtask_of ~version:"1.1" path in
   let http = TraceHelper.inject_span_into_req tracing http in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (#6165))
   let transport =
     if Pool_role.is_master () then
       Unix Xapi_globs.unix_domain_socket
diff --git a/ocaml/xapi/xapi_periodic_scheduler_init.ml b/ocaml/xapi/xapi_periodic_scheduler_init.ml
index 6300f89db2..1bd13d5f6d 100644
--- a/ocaml/xapi/xapi_periodic_scheduler_init.ml
+++ b/ocaml/xapi/xapi_periodic_scheduler_init.ml
@@ -129,9 +129,10 @@ let register ~__context =
       )
   ) ;
   let stunnel_period = !Stunnel_cache.max_idle /. 2. in
-  Xapi_periodic_scheduler.add_to_queue "Check stunnel cache expiry"
-    (Xapi_periodic_scheduler.Periodic stunnel_period) stunnel_period
-    Stunnel_cache.gc ;
+  Xapi_stdext_threads_scheduler.Scheduler.add_to_queue
+    "Check stunnel cache expiry"
+    (Xapi_stdext_threads_scheduler.Scheduler.Periodic stunnel_period)
+    stunnel_period Stunnel_cache.gc ;
   if
     master
     && Db.Pool.get_update_sync_enabled ~__context
```
edwintorok added a commit to edwintorok/xen-api that referenced this pull request Dec 10, 2024
This is a single line change to move qemu to the proper cgroup.

Qemu moves itself to the root cgroup, which makes it a sibling of
control.slice. All toolstack processes are in control.slice, and even
though control.slice gets 10x share of CPU, if we have 1000 qemu
processes there is still a big imbalance (toolstack gets 10/1010 share
of the CPU)

Qemu-wrapper already has the ability to move the process to another
cgroup, so use it. tapdisk and qemu-datapath are already in vm.slice, so
move qemu-dm there too.

Checked that qemu ends up in the correct slice:
```
cat /proc/$(pgrep qemu-dm\*|head -n1)/cgroup
8:memory:/vm.slice
7:blkio:/vm.slice
6:net_cls,net_prio:/
5:freezer:/
4:cpu,cpuacct:/vm.slice
3:cpuset:/
2:devices:/
1:name=systemd:/vm.slice/[email protected]
```

Tested with a script that control.slice and vm.slice are indeed properly
limiting CPU usage, and control.slice gets the expected share of CPU
even when vm.slice has a lot more processes:
```
#!/bin/bash
set -eu

# $$ is the pid of the main shell
# Need to use $BASHPID to get pid of subshell

for SLICE in system.slice control.slice vm.slice; do
	for i in $(seq 1 16); do
		(echo $BASHPID >/sys/fs/cgroup/cpu,cpuacct/${SLICE}/tasks; while true; do echo -n; done)&
	done
done

for i in $(seq 17 1000); do
	(echo $BASHPID >/sys/fs/cgroup/cpu,cpuacct/vm.slice/tasks; while true; do echo -n; done)&
done
```

```
329178 root      20   0  113216    216     16 R  92.4  0.0   0:07.50 bash
 329171 root      20   0  113216    216     16 R  84.4  0.0   0:07.05 bash
 329177 root      20   0  113216    216     16 R  84.4  0.0   0:06.97 bash
 329170 root      20   0  113216    216     16 R  84.1  0.0   0:06.65 bash
 329180 root      20   0  113216    216     16 R  84.1  0.0   0:06.94 bash
 329183 root      20   0  113216    220     16 R  81.8  0.0   0:06.93 bash
 329174 root      20   0  113216    216     16 R  80.6  0.0   0:06.90 bash
 329181 root      20   0  113216    216     16 R  79.9  0.0   0:07.13 bash
 329179 root      20   0  113216    216     16 R  79.3  0.0   0:06.78 bash
 329184 root      20   0  113216    220     16 R  79.3  0.0   0:06.55 bash
 329175 root      20   0  113216    216     16 R  79.0  0.0   0:06.87 bash
 329172 root      20   0  113216    216     16 R  78.7  0.0   0:07.29 bash
 329173 root      20   0  113216    216     16 R  78.7  0.0   0:06.86 bash
 329169 root      20   0  113216    216     16 R  77.4  0.0   0:07.04 bash
 329182 root      20   0  113216    220     16 R  76.4  0.0   0:06.75 bash
 329176 root      20   0  113216    216     16 R  76.1  0.0   0:06.76 bash
 329152 root      20   0  113216    212     16 R  11.1  0.0   0:00.83 bash
 329155 root      20   0  113216    212     16 R   9.9  0.0   0:00.87 bash
 329160 root      20   0  113216    212     16 R   8.6  0.0   0:00.70 bash
 329161 root      20   0  113216    212     16 R   8.3  0.0   0:00.71 bash
 329156 root      20   0  113216    212     16 R   8.0  0.0   0:00.66 bash
 329158 root      20   0  113216    212     16 R   8.0  0.0   0:00.66 bash
 329163 root      20   0  113216    212     16 R   8.0  0.0   0:00.88 bash
 329154 root      20   0  113216    212     16 R   7.6  0.0   0:00.66 bash
 329159 root      20   0  113216    212     16 R   7.6  0.0   0:00.67 bash
 329164 root      20   0  113216    212     16 R   7.6  0.0   0:00.68 bash
 329165 root      20   0  113216    212     16 R   7.6  0.0   0:00.79 bash
 329166 root      20   0  113216    216     16 R   7.6  0.0   0:00.65 bash
 329153 root      20   0  113216    212     16 R   7.3  0.0   0:00.67 bash
 329157 root      20   0  113216    212     16 R   7.3  0.0   0:00.73 bash
 329162 root      20   0  113216    212     16 R   7.3  0.0   0:00.64 bash
 329167 root      20   0  113216    216     16 R   7.3  0.0   0:00.64 bash
```

And verifying some of the high CPU usage `bash` processes vs some of the
low ones: the high ones are in `control.slice`, the low ones are in
other slices.
edwintorok added a commit to edwintorok/xen-api that referenced this pull request Dec 10, 2024
And solve conflicts.

The conflict resolution can be reviewed locally with this command if you
have a new enough version of `git`:
```
git log --remerge-diff -1 81146d223d3d5cd449105ecef713cfd57e2c1853
```

The conflicts are mostly due to:
* with_tracing being removed from the Http library
* the xapi_periodic_scheduler being moved to xapi-stdext-threads
* a slightly different version of the concurrent PR being merged between
the 2 branches (`vm` instead of `vm'`)

For convenience here is the output of that command:
```diff
commit 81146d223d3d5cd449105ecef713cfd57e2c1853
Merge: 59da2e0 d8baca7
Author: Edwin Török <[email protected]>
Date:   Tue Dec 10 13:43:23 2024 +0000

    Merge master into feature/perf

    Fix conflicts in http.ml (with_tracing got moved),
    and periodic scheduler (got moved to xapi-stdext-threads).

    Signed-off-by: Edwin Török <[email protected]>

diff --git a/ocaml/libs/http-lib/http.ml b/ocaml/libs/http-lib/http.ml
remerge CONFLICT (content): Merge conflict in ocaml/libs/http-lib/http.ml
index ca635e2ba0..c979e1f7d9 100644
--- a/ocaml/libs/http-lib/http.ml
+++ b/ocaml/libs/http-lib/http.ml
@@ -132,16 +132,8 @@ module Hdr = struct

   let location = "location"

-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
   let originator = "originator"

-  let traceparent = "traceparent"
-
-||||||| 77dd474
-  let traceparent = "traceparent"
-
-=======
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
   let hsts = "strict-transport-security"
 end

@@ -684,7 +676,6 @@ module Request = struct
     let headers, body = to_headers_and_body x in
     let frame_header = if x.frame then make_frame_header headers else "" in
     frame_header ^ headers ^ body
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))

   let with_originator_of req f =
     Option.iter
@@ -693,55 +684,6 @@ module Request = struct
         f originator
       )
       req
-
-  let traceparent_of req =
-    let open Tracing in
-    let ( let* ) = Option.bind in
-    let* traceparent = req.traceparent in
-    let* span_context = SpanContext.of_traceparent traceparent in
-    let span = Tracer.span_of_span_context span_context req.uri in
-    Some span
-
-  let with_tracing ?attributes ~name req f =
-    let open Tracing in
-    let parent = traceparent_of req in
-    with_child_trace ?attributes parent ~name (fun (span : Span.t option) ->
-        match span with
-        | Some span ->
-            let traceparent =
-              Some (span |> Span.get_context |> SpanContext.to_traceparent)
-            in
-            let req = {req with traceparent} in
-            f req
-        | None ->
-            f req
-    )
-||||||| 77dd474
-
-  let traceparent_of req =
-    let open Tracing in
-    let ( let* ) = Option.bind in
-    let* traceparent = req.traceparent in
-    let* span_context = SpanContext.of_traceparent traceparent in
-    let span = Tracer.span_of_span_context span_context req.uri in
-    Some span
-
-  let with_tracing ?attributes ~name req f =
-    let open Tracing in
-    let parent = traceparent_of req in
-    with_child_trace ?attributes parent ~name (fun (span : Span.t option) ->
-        match span with
-        | Some span ->
-            let traceparent =
-              Some (span |> Span.get_context |> SpanContext.to_traceparent)
-            in
-            let req = {req with traceparent} in
-            f req
-        | None ->
-            f req
-    )
-=======
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
 end

 module Response = struct
diff --git a/ocaml/libs/http-lib/http.mli b/ocaml/libs/http-lib/http.mli
remerge CONFLICT (content): Merge conflict in ocaml/libs/http-lib/http.mli
index e77a9ebd5a..114ddbc4f4 100644
--- a/ocaml/libs/http-lib/http.mli
+++ b/ocaml/libs/http-lib/http.mli
@@ -126,22 +126,8 @@ module Request : sig

   val to_wire_string : t -> string
   (** [to_wire_string t] returns a string which could be sent to a server *)
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))

   val with_originator_of : t option -> (string option -> unit) -> unit
-
-  val traceparent_of : t -> Tracing.Span.t option
-
-  val with_tracing :
-    ?attributes:(string * string) list -> name:string -> t -> (t -> 'a) -> 'a
-||||||| 77dd474
-
-  val traceparent_of : t -> Tracing.Span.t option
-
-  val with_tracing :
-    ?attributes:(string * string) list -> name:string -> t -> (t -> 'a) -> 'a
-=======
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
 end

 (** Parsed form of the HTTP response *)
diff --git a/ocaml/tests/dune b/ocaml/tests/dune
index b51bbca8b8..ce8fe96c19 100644
--- a/ocaml/tests/dune
+++ b/ocaml/tests/dune
@@ -118,6 +118,7 @@
     xapi-types
     xapi-stdext-date
     xapi-stdext-threads
+    xapi-stdext-threads.scheduler
     xapi-stdext-unix
     xml-light2
     yojson
diff --git a/ocaml/tests/test_event.ml b/ocaml/tests/test_event.ml
index d36dba90ef..821bb3bb52 100644
--- a/ocaml/tests/test_event.ml
+++ b/ocaml/tests/test_event.ml
@@ -287,7 +287,7 @@ let test_short_oneshot () =
     started := true ;
     Condition.broadcast cond ;
     Mutex.unlock m ;
-    Xapi_periodic_scheduler.loop ()
+    Xapi_stdext_threads_scheduler.Scheduler.loop ()
   in
   ignore (Thread.create scheduler ()) ;
   (* ensure scheduler sees an empty queue , by waiting for it to start *)
@@ -303,8 +303,8 @@ let test_short_oneshot () =
   let fired = Atomic.make false in
   let fire () = Atomic.set fired true in
   let task = "test_oneshot" in
-  Xapi_periodic_scheduler.add_to_queue task Xapi_periodic_scheduler.OneShot 1.
-    fire ;
+  Xapi_stdext_threads_scheduler.Scheduler.add_to_queue task
+    Xapi_stdext_threads_scheduler.Scheduler.OneShot 1. fire ;
   Thread.delay 2. ;
   assert (Atomic.get fired)

diff --git a/ocaml/xapi-storage-script/main.ml b/ocaml/xapi-storage-script/main.ml
remerge CONFLICT (content): Merge conflict in ocaml/xapi-storage-script/main.ml
index 1ec4d9dcb1..cba7ec89d5 100644
--- a/ocaml/xapi-storage-script/main.ml
+++ b/ocaml/xapi-storage-script/main.ml
@@ -66,7 +66,6 @@ let backend_backtrace_error name args backtrace =
 let missing_uri () =
   backend_error "MISSING_URI" ["Please include a URI in the device-config"]

-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
 (** return a unique 'domain' string for Dom0, so that we can plug disks
   multiple times (e.g. for copy).

@@ -84,26 +83,6 @@ let domain_of ~dp ~vm =
   | _ ->
       vm

-||||||| 77dd474
-=======
-(** return a unique 'domain' string for Dom0, so that we can plug disks
-  multiple times (e.g. for copy).
-
-  XAPI should give us a unique 'dp' (datapath) string, e.g. a UUID for storage migration,
-  or vbd/domid/device.
-  For regular guests keep the domain as passed by XAPI (an integer).
- *)
-let domain_of ~dp ~vm' =
-  let vm = Storage_interface.Vm.string_of vm' in
-  match vm with
-  | "0" ->
-      (* SM tries to use this in filesystem paths, so cannot have /,
-         and systemd might be a bit unhappy with - *)
-      "u0-" ^ dp |> String.map (function '/' | '-' -> '_' | c -> c)
-  | _ ->
-      vm
-
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
 (** Functions to wrap calls to the above client modules and convert their
     exceptions and errors into SMAPIv2 errors of type
     [Storage_interface.Exception.exnty]. The above client modules should only
@@ -1476,21 +1455,9 @@ let bind ~volume_script_dir =
     |> wrap
   in
   S.VDI.introduce vdi_introduce_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
   let vdi_attach3_impl dbg dp sr vdi' vm _readwrite =
-||||||| 77dd474
-  let vdi_attach3_impl dbg _dp sr vdi' vm' _readwrite =
-=======
-  let vdi_attach3_impl dbg dp sr vdi' vm' _readwrite =
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
     (let vdi = Storage_interface.Vdi.string_of vdi' in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
      let domain = domain_of ~dp ~vm in
-||||||| 77dd474
-     let domain = Storage_interface.Vm.string_of vm' in
-=======
-     let domain = domain_of ~dp ~vm' in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
      vdi_attach_common dbg sr vdi domain >>>= fun response ->
      let convert_implementation = function
        | Xapi_storage.Data.XenDisk {params; extra; backend_type} ->
@@ -1512,21 +1479,9 @@ let bind ~volume_script_dir =
     |> wrap
   in
   S.VDI.attach3 vdi_attach3_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
   let vdi_activate_common dbg dp sr vdi' vm readonly =
-||||||| 77dd474
-  let vdi_activate_common dbg sr vdi' vm' readonly =
-=======
-  let vdi_activate_common dbg dp sr vdi' vm' readonly =
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
     (let vdi = Storage_interface.Vdi.string_of vdi' in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
      let domain = domain_of ~dp ~vm in
-||||||| 77dd474
-     let domain = Storage_interface.Vm.string_of vm' in
-=======
-     let domain = domain_of ~dp ~vm' in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
      Attached_SRs.find sr >>>= fun sr ->
      (* Discover the URIs using Volume.stat *)
      stat ~dbg ~sr ~vdi >>>= fun response ->
@@ -1551,45 +1506,17 @@ let bind ~volume_script_dir =
     )
     |> wrap
   in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
   let vdi_activate3_impl dbg dp sr vdi' vm =
     vdi_activate_common dbg dp sr vdi' vm false
-||||||| 77dd474
-  let vdi_activate3_impl dbg _dp sr vdi' vm' =
-    vdi_activate_common dbg sr vdi' vm' false
-=======
-  let vdi_activate3_impl dbg dp sr vdi' vm' =
-    vdi_activate_common dbg dp sr vdi' vm' false
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
   in
   S.VDI.activate3 vdi_activate3_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
   let vdi_activate_readonly_impl dbg dp sr vdi' vm =
     vdi_activate_common dbg dp sr vdi' vm true
-||||||| 77dd474
-  let vdi_activate_readonly_impl dbg _dp sr vdi' vm' =
-    vdi_activate_common dbg sr vdi' vm' true
-=======
-  let vdi_activate_readonly_impl dbg dp sr vdi' vm' =
-    vdi_activate_common dbg dp sr vdi' vm' true
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
   in
   S.VDI.activate_readonly vdi_activate_readonly_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
   let vdi_deactivate_impl dbg dp sr vdi' vm =
-||||||| 77dd474
-  let vdi_deactivate_impl dbg _dp sr vdi' vm' =
-=======
-  let vdi_deactivate_impl dbg dp sr vdi' vm' =
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
     (let vdi = Storage_interface.Vdi.string_of vdi' in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
      let domain = domain_of ~dp ~vm in
-||||||| 77dd474
-     let domain = Storage_interface.Vm.string_of vm' in
-=======
-     let domain = domain_of ~dp ~vm' in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
      Attached_SRs.find sr >>>= fun sr ->
      (* Discover the URIs using Volume.stat *)
      stat ~dbg ~sr ~vdi >>>= fun response ->
@@ -1610,21 +1537,9 @@ let bind ~volume_script_dir =
     |> wrap
   in
   S.VDI.deactivate vdi_deactivate_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
   let vdi_detach_impl dbg dp sr vdi' vm =
-||||||| 77dd474
-  let vdi_detach_impl dbg _dp sr vdi' vm' =
-=======
-  let vdi_detach_impl dbg dp sr vdi' vm' =
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
     (let vdi = Storage_interface.Vdi.string_of vdi' in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
      let domain = domain_of ~dp ~vm in
-||||||| 77dd474
-     let domain = Storage_interface.Vm.string_of vm' in
-=======
-     let domain = domain_of ~dp ~vm' in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
      Attached_SRs.find sr >>>= fun sr ->
      (* Discover the URIs using Volume.stat *)
      stat ~dbg ~sr ~vdi >>>= fun response ->
@@ -1732,21 +1647,9 @@ let bind ~volume_script_dir =
   S.VDI.epoch_end vdi_epoch_end_impl ;
   let vdi_set_persistent_impl _dbg _sr _vdi _persistent = return () |> wrap in
   S.VDI.set_persistent vdi_set_persistent_impl ;
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
   let dp_destroy2 dbg dp sr vdi' vm _allow_leak =
-||||||| 77dd474
-  let dp_destroy2 dbg _dp sr vdi' vm' _allow_leak =
-=======
-  let dp_destroy2 dbg dp sr vdi' vm' _allow_leak =
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
     (let vdi = Storage_interface.Vdi.string_of vdi' in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
      let domain = domain_of ~dp ~vm in
-||||||| 77dd474
-     let domain = Storage_interface.Vm.string_of vm' in
-=======
-     let domain = domain_of ~dp ~vm' in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
      Attached_SRs.find sr >>>= fun sr ->
      (* Discover the URIs using Volume.stat *)
      stat ~dbg ~sr ~vdi >>>= fun response ->
@@ -1888,17 +1791,7 @@ let rec diff a b =
   | a :: aa ->
       if List.mem a b then diff aa b else a :: diff aa b

-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
 let concurrent = ref true
-||||||| 77dd474
-(* default false due to bugs in SMAPIv3 plugins,
-   once they are fixed this should be set to true *)
-let concurrent = ref false
-=======
-(* default false due to bugs in SMAPIv3 plugins,
-   once they are fixed this should be set to true *)
-let concurrent = ref true
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))

 type reload = All | Files of string list | Nothing

diff --git a/ocaml/xapi/helpers.ml b/ocaml/xapi/helpers.ml
remerge CONFLICT (content): Merge conflict in ocaml/xapi/helpers.ml
index ca57afb8d8..1175b6aa03 100644
--- a/ocaml/xapi/helpers.ml
+++ b/ocaml/xapi/helpers.ml
@@ -410,20 +410,14 @@ let make_rpc ~__context rpc : Rpc.response =
   let subtask_of = Ref.string_of (Context.get_task_id __context) in
   let open Xmlrpc_client in
   let tracing = Context.set_client_span __context in
-<<<<<<< 59da2e0 (CA-388564: move qemu-dm to vm.slice (xapi-project#6150))
   let dorpc, path =
     if !Xapi_globs.use_xmlrpc then
       (XMLRPC_protocol.rpc, "/")
     else
       (JSONRPC_protocol.rpc, "/jsonrpc")
   in
-  let http = xmlrpc ~subtask_of ~version:"1.1" path ~tracing in
-||||||| 77dd474
-  let http = xmlrpc ~subtask_of ~version:"1.1" "/" ~tracing in
-=======
-  let http = xmlrpc ~subtask_of ~version:"1.1" "/" in
+  let http = xmlrpc ~subtask_of ~version:"1.1" path in
   let http = TraceHelper.inject_span_into_req tracing http in
->>>>>>> d8baca7 (CA-390025: do not override SR's client-set metadata on update  (xapi-project#6165))
   let transport =
     if Pool_role.is_master () then
       Unix Xapi_globs.unix_domain_socket
diff --git a/ocaml/xapi/xapi_periodic_scheduler_init.ml b/ocaml/xapi/xapi_periodic_scheduler_init.ml
index 6300f89db2..1bd13d5f6d 100644
--- a/ocaml/xapi/xapi_periodic_scheduler_init.ml
+++ b/ocaml/xapi/xapi_periodic_scheduler_init.ml
@@ -129,9 +129,10 @@ let register ~__context =
       )
   ) ;
   let stunnel_period = !Stunnel_cache.max_idle /. 2. in
-  Xapi_periodic_scheduler.add_to_queue "Check stunnel cache expiry"
-    (Xapi_periodic_scheduler.Periodic stunnel_period) stunnel_period
-    Stunnel_cache.gc ;
+  Xapi_stdext_threads_scheduler.Scheduler.add_to_queue
+    "Check stunnel cache expiry"
+    (Xapi_stdext_threads_scheduler.Scheduler.Periodic stunnel_period)
+    stunnel_period Stunnel_cache.gc ;
   if
     master
     && Db.Pool.get_update_sync_enabled ~__context
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants