libprocessgroup: Add MaxActivationDepth

Cgroup v2 controllers can be enabled in a subtree of the shared
hierarchy. That allows users to limit the number of cgroups with a
controller enabled to less than the total number of cgroups. [1]

There are costs for each cgroup. Kernel memory is used for each cgroup,
plus additional memory for each active controller in each cgroup. Some
kernel operations scale with the number of cgroups (with a given
controller enabled), so it can be desirable to minimize the number of
cgroups with that controller enabled.

This change allows each v2 controller configuration to specify a maximum
activation depth, past which the controller will not be activated deeper
in the Android cgroup v2 hierarchy. The hierarchy root is defined as
depth 0. MaxActivationDepth is the field name for this purpose for
controllers in the Controllers array under Cgroups2 in cgroups.json.

Here are two examples:

"MaxActivationDepth": 1
This will activate the controller in every per-application cgroup, but
not in the per-process cgroups below.

/sys/fs/cgroup                depth=0  active=true  (controller listed in cgroup.subtree_control)
/sys/fs/cgroup/uid_0          depth=1  active=true  (controller NOT listed in cgroup.subtree_control)
/sys/fs/cgroup/uid_0/pid_100  depth=2  active=false (controller NOT listed in cgroup.subtree_control)

This can also be used with
PRODUCT_CGROUP_V2_SYS_APP_ISOLATION_ENABLED := true.

"MaxActivationDepth": 1
This will activate the controller only at the app / system level, but
not in per-application cgroups below. This results in a total of only
3 cgroups with the controller enabled (root, apps, system).

/sys/fs/cgroup                         depth=0  active=true  (controller listed in cgroup.subtree_control)
/sys/fs/cgroup/apps                    depth=1  active=true  (controller NOT listed in cgroup.subtree_control)
/sys/fs/cgroup/apps/uid_10000          depth=2  active=false (controller NOT listed in cgroup.subtree_control)
/sys/fs/cgroup/apps/uid_10000/pid_100  depth=3  active=false (controller NOT listed in cgroup.subtree_control)

[1] https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#enabling-and-disabling

Bug: 346584259
Test: Cuttlefish with memcg v2
Change-Id: I62109ea935261c51fc30b2054c4d28d0360f7985
diff --git a/libprocessgroup/Android.bp b/libprocessgroup/Android.bp
index d40be9f..33e00bc 100644
--- a/libprocessgroup/Android.bp
+++ b/libprocessgroup/Android.bp
@@ -84,6 +84,7 @@
     header_libs: [
         "libcutils_headers",
         "libprocessgroup_headers",
+        "libprocessgroup_util",
     ],
     export_include_dirs: ["include"],
     export_header_lib_headers: [
diff --git a/libprocessgroup/cgroup_map.cpp b/libprocessgroup/cgroup_map.cpp
index ebc0599..52b5afe 100644
--- a/libprocessgroup/cgroup_map.cpp
+++ b/libprocessgroup/cgroup_map.cpp
@@ -28,6 +28,7 @@
 #include <android-base/strings.h>
 #include <cgroup_map.h>
 #include <processgroup/processgroup.h>
+#include <processgroup/util.h>
 
 using android::base::StartsWith;
 using android::base::StringPrintf;
@@ -216,7 +217,13 @@
         for (uint32_t i = 0; i < controller_count; ++i) {
             const ACgroupController* controller = ACgroupFile_getController(i);
             const uint32_t flags = ACgroupController_getFlags(controller);
-            if (flags & CGROUPRC_CONTROLLER_FLAG_NEEDS_ACTIVATION) {
+            uint32_t max_activation_depth = UINT32_MAX;
+            if (__builtin_available(android 36, *)) {
+                max_activation_depth = ACgroupController_getMaxActivationDepth(controller);
+            }
+            const int depth = util::GetCgroupDepth(ACgroupController_getPath(controller), path);
+
+            if (flags & CGROUPRC_CONTROLLER_FLAG_NEEDS_ACTIVATION && depth < max_activation_depth) {
                 std::string str("+");
                 str.append(ACgroupController_getName(controller));
                 if (!WriteStringToFile(str, path + "/cgroup.subtree_control")) {
diff --git a/libprocessgroup/cgrouprc/cgroup_controller.cpp b/libprocessgroup/cgrouprc/cgroup_controller.cpp
index 5a326e5..889b3be 100644
--- a/libprocessgroup/cgrouprc/cgroup_controller.cpp
+++ b/libprocessgroup/cgrouprc/cgroup_controller.cpp
@@ -32,6 +32,11 @@
     return controller->flags();
 }
 
+uint32_t ACgroupController_getMaxActivationDepth(const ACgroupController* controller) {
+    CHECK(controller != nullptr);
+    return controller->max_activation_depth();
+}
+
 const char* ACgroupController_getName(const ACgroupController* controller) {
     CHECK(controller != nullptr);
     return controller->name();
diff --git a/libprocessgroup/cgrouprc/include/android/cgrouprc.h b/libprocessgroup/cgrouprc/include/android/cgrouprc.h
index e704a36..3a57df5 100644
--- a/libprocessgroup/cgrouprc/include/android/cgrouprc.h
+++ b/libprocessgroup/cgrouprc/include/android/cgrouprc.h
@@ -79,6 +79,14 @@
         const ACgroupController*) __INTRODUCED_IN(30);
 
 /**
+ * Returns the maximum activation depth of the given controller.
+ * Only applicable to cgroup v2 controllers.
+ * Returns UINT32_MAX if no maximum activation depth is set.
+ */
+__attribute__((warn_unused_result, weak)) uint32_t ACgroupController_getMaxActivationDepth(
+        const ACgroupController* controller) __INTRODUCED_IN(36);
+
+/**
  * Returns the name of the given controller.
  * If the given controller is null, return nullptr.
  */
diff --git a/libprocessgroup/cgrouprc/libcgrouprc.map.txt b/libprocessgroup/cgrouprc/libcgrouprc.map.txt
index b62b10f..30bd25f 100644
--- a/libprocessgroup/cgrouprc/libcgrouprc.map.txt
+++ b/libprocessgroup/cgrouprc/libcgrouprc.map.txt
@@ -16,3 +16,10 @@
   local:
     *;
 };
+
+LIBCGROUPRC_36 { # introduced=36
+  global:
+    ACgroupController_getMaxActivationDepth; # llndk=202504 systemapi
+  local:
+    *;
+};
diff --git a/libprocessgroup/cgrouprc_format/cgroup_controller.cpp b/libprocessgroup/cgrouprc_format/cgroup_controller.cpp
index 56e67df..0dd909a 100644
--- a/libprocessgroup/cgrouprc_format/cgroup_controller.cpp
+++ b/libprocessgroup/cgrouprc_format/cgroup_controller.cpp
@@ -21,13 +21,11 @@
 namespace format {
 
 CgroupController::CgroupController(uint32_t version, uint32_t flags, const std::string& name,
-                                   const std::string& path)
-{
+                                   const std::string& path, uint32_t max_activation_depth)
+    : version_(version), flags_(flags), max_activation_depth_(max_activation_depth) {
     // strlcpy isn't available on host. Although there is an implementation
     // in licutils, libcutils itself depends on libcgrouprc_format, causing
     // a circular dependency.
-    version_ = version;
-    flags_ = flags;
     strncpy(name_, name.c_str(), sizeof(name_) - 1);
     name_[sizeof(name_) - 1] = '\0';
     strncpy(path_, path.c_str(), sizeof(path_) - 1);
@@ -42,6 +40,10 @@
     return flags_;
 }
 
+uint32_t CgroupController::max_activation_depth() const {
+    return max_activation_depth_;
+}
+
 const char* CgroupController::name() const {
     return name_;
 }
diff --git a/libprocessgroup/cgrouprc_format/include/processgroup/format/cgroup_controller.h b/libprocessgroup/cgrouprc_format/include/processgroup/format/cgroup_controller.h
index 9427a1c..c0c1f60 100644
--- a/libprocessgroup/cgrouprc_format/include/processgroup/format/cgroup_controller.h
+++ b/libprocessgroup/cgrouprc_format/include/processgroup/format/cgroup_controller.h
@@ -29,10 +29,11 @@
   public:
     CgroupController() = default;
     CgroupController(uint32_t version, uint32_t flags, const std::string& name,
-                     const std::string& path);
+                     const std::string& path, uint32_t max_activation_depth);
 
     uint32_t version() const;
     uint32_t flags() const;
+    uint32_t max_activation_depth() const;
     const char* name() const;
     const char* path() const;
 
@@ -44,6 +45,7 @@
 
     uint32_t version_ = 0;
     uint32_t flags_ = 0;
+    uint32_t max_activation_depth_ = UINT32_MAX;
     char name_[CGROUP_NAME_BUF_SZ] = {};
     char path_[CGROUP_PATH_BUF_SZ] = {};
 };
diff --git a/libprocessgroup/profiles/cgroups.proto b/libprocessgroup/profiles/cgroups.proto
index f2de345..d2fd472 100644
--- a/libprocessgroup/profiles/cgroups.proto
+++ b/libprocessgroup/profiles/cgroups.proto
@@ -24,7 +24,7 @@
     Cgroups2 cgroups2 = 2 [json_name = "Cgroups2"];
 }
 
-// Next: 8
+// Next: 9
 message Cgroup {
     string controller = 1 [json_name = "Controller"];
     string path = 2 [json_name = "Path"];
@@ -36,6 +36,7 @@
 // https://developers.google.com/protocol-buffers/docs/proto3#default
     bool needs_activation = 6 [json_name = "NeedsActivation"];
     bool is_optional = 7 [json_name = "Optional"];
+    uint32 max_activation_depth = 8 [json_name = "MaxActivationDepth"];
 }
 
 // Next: 6
diff --git a/libprocessgroup/setup/Android.bp b/libprocessgroup/setup/Android.bp
index 1e0783a..76f0a11 100644
--- a/libprocessgroup/setup/Android.bp
+++ b/libprocessgroup/setup/Android.bp
@@ -37,6 +37,7 @@
     ],
     header_libs: [
         "libprocessgroup_headers",
+        "libprocessgroup_util",
     ],
     export_header_lib_headers: [
         "libprocessgroup_headers",
diff --git a/libprocessgroup/setup/cgroup_descriptor.h b/libprocessgroup/setup/cgroup_descriptor.h
index 9982bfc..06ce186 100644
--- a/libprocessgroup/setup/cgroup_descriptor.h
+++ b/libprocessgroup/setup/cgroup_descriptor.h
@@ -30,7 +30,8 @@
 class CgroupDescriptor {
   public:
     CgroupDescriptor(uint32_t version, const std::string& name, const std::string& path,
-                     mode_t mode, const std::string& uid, const std::string& gid, uint32_t flags);
+                     mode_t mode, const std::string& uid, const std::string& gid, uint32_t flags,
+                     uint32_t max_activation_depth);
 
     const format::CgroupController* controller() const { return &controller_; }
     mode_t mode() const { return mode_; }
diff --git a/libprocessgroup/setup/cgroup_map_write.cpp b/libprocessgroup/setup/cgroup_map_write.cpp
index 1b26fbc..bd41874 100644
--- a/libprocessgroup/setup/cgroup_map_write.cpp
+++ b/libprocessgroup/setup/cgroup_map_write.cpp
@@ -42,6 +42,7 @@
 #include <processgroup/format/cgroup_file.h>
 #include <processgroup/processgroup.h>
 #include <processgroup/setup.h>
+#include <processgroup/util.h>
 
 #include "../build_flags.h"
 #include "cgroup_descriptor.h"
@@ -173,9 +174,15 @@
         controller_flags |= CGROUPRC_CONTROLLER_FLAG_OPTIONAL;
     }
 
+    uint32_t max_activation_depth = UINT32_MAX;
+    if (cgroup.isMember("MaxActivationDepth")) {
+        max_activation_depth = cgroup["MaxActivationDepth"].asUInt();
+    }
+
     CgroupDescriptor descriptor(
             cgroups_version, name, path, std::strtoul(cgroup["Mode"].asString().c_str(), 0, 8),
-            cgroup["UID"].asString(), cgroup["GID"].asString(), controller_flags);
+            cgroup["UID"].asString(), cgroup["GID"].asString(), controller_flags,
+            max_activation_depth);
 
     auto iter = descriptors->find(name);
     if (iter == descriptors->end()) {
@@ -324,7 +331,8 @@
         return false;
     }
 
-    if (controller->flags() & CGROUPRC_CONTROLLER_FLAG_NEEDS_ACTIVATION) {
+    if (controller->flags() & CGROUPRC_CONTROLLER_FLAG_NEEDS_ACTIVATION &&
+        controller->max_activation_depth() > 0) {
         std::string str = "+";
         str += controller->name();
         std::string path = controller->path();
@@ -433,8 +441,12 @@
 
 CgroupDescriptor::CgroupDescriptor(uint32_t version, const std::string& name,
                                    const std::string& path, mode_t mode, const std::string& uid,
-                                   const std::string& gid, uint32_t flags = 0)
-    : controller_(version, flags, name, path), mode_(mode), uid_(uid), gid_(gid) {}
+                                   const std::string& gid, uint32_t flags,
+                                   uint32_t max_activation_depth)
+    : controller_(version, flags, name, path, max_activation_depth),
+      mode_(mode),
+      uid_(uid),
+      gid_(gid) {}
 
 void CgroupDescriptor::set_mounted(bool mounted) {
     uint32_t flags = controller_.flags();
@@ -502,8 +514,11 @@
     for (const auto& [name, descriptor] : descriptors) {
         const format::CgroupController* controller = descriptor.controller();
         std::uint32_t flags = controller->flags();
+        std::uint32_t max_activation_depth = controller->max_activation_depth();
+        const int depth = util::GetCgroupDepth(controller->path(), path);
+
         if (controller->version() == 2 && name != CGROUPV2_HIERARCHY_NAME &&
-            flags & CGROUPRC_CONTROLLER_FLAG_NEEDS_ACTIVATION) {
+            flags & CGROUPRC_CONTROLLER_FLAG_NEEDS_ACTIVATION && depth < max_activation_depth) {
             std::string str("+");
             str += controller->name();
             if (!android::base::WriteStringToFile(str, path + "/cgroup.subtree_control")) {
diff --git a/libprocessgroup/util/Android.bp b/libprocessgroup/util/Android.bp
new file mode 100644
index 0000000..4a940b7
--- /dev/null
+++ b/libprocessgroup/util/Android.bp
@@ -0,0 +1,47 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_team: "trendy_team_android_kernel",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_headers {
+    name: "libprocessgroup_util",
+    vendor_available: true,
+    product_available: true,
+    ramdisk_available: true,
+    vendor_ramdisk_available: true,
+    recovery_available: true,
+    host_supported: true,
+    native_bridge_supported: true,
+    apex_available: [
+        "//apex_available:platform",
+        "//apex_available:anyapex",
+    ],
+    min_sdk_version: "30",
+    export_include_dirs: [
+        "include",
+    ],
+    defaults: ["libprocessgroup_build_flags_cc"],
+}
+
+cc_test {
+    name: "libprocessgroup_util_test",
+    header_libs: ["libprocessgroup_util"],
+    srcs: ["tests/util.cpp"],
+    test_suites: ["general-tests"],
+}
diff --git a/libprocessgroup/util/OWNERS b/libprocessgroup/util/OWNERS
new file mode 100644
index 0000000..54ea400
--- /dev/null
+++ b/libprocessgroup/util/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 1293033
+surenb@google.com
+tjmercier@google.com
diff --git a/libprocessgroup/util/TEST_MAPPING b/libprocessgroup/util/TEST_MAPPING
new file mode 100644
index 0000000..6ae2658
--- /dev/null
+++ b/libprocessgroup/util/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "postsubmit": [
+    {
+      "name": "libprocessgroup_util_test"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/libprocessgroup/util/include/processgroup/util.h b/libprocessgroup/util/include/processgroup/util.h
new file mode 100644
index 0000000..5240744
--- /dev/null
+++ b/libprocessgroup/util/include/processgroup/util.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <iterator>
+#include <string>
+
+namespace util {
+
+namespace internal {
+
+const char SEP = '/';
+
+std::string DeduplicateAndTrimSeparators(const std::string& path) {
+    bool lastWasSep = false;
+    std::string ret;
+
+    std::copy_if(path.begin(), path.end(), std::back_inserter(ret), [&lastWasSep](char c) {
+        if (lastWasSep) {
+            if (c == SEP) return false;
+            lastWasSep = false;
+        } else if (c == SEP) {
+            lastWasSep = true;
+        }
+        return true;
+    });
+
+    if (ret.length() > 1 && ret.back() == SEP) ret.pop_back();
+
+    return ret;
+}
+
+}  // namespace internal
+
+unsigned int GetCgroupDepth(const std::string& controller_root, const std::string& cgroup_path) {
+    const std::string deduped_root = internal::DeduplicateAndTrimSeparators(controller_root);
+    const std::string deduped_path = internal::DeduplicateAndTrimSeparators(cgroup_path);
+
+    if (deduped_root.empty() || deduped_path.empty() || !deduped_path.starts_with(deduped_root))
+        return 0;
+
+    return std::count(deduped_path.begin() + deduped_root.size(), deduped_path.end(),
+                      internal::SEP);
+}
+
+}  // namespace util
diff --git a/libprocessgroup/util/tests/util.cpp b/libprocessgroup/util/tests/util.cpp
new file mode 100644
index 0000000..1de7d6f
--- /dev/null
+++ b/libprocessgroup/util/tests/util.cpp
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <processgroup/util.h>
+
+#include "gtest/gtest.h"
+
+using util::GetCgroupDepth;
+
+TEST(EmptyInputs, bothEmpty) {
+    EXPECT_EQ(GetCgroupDepth({}, {}), 0);
+}
+
+TEST(EmptyInputs, rootEmpty) {
+    EXPECT_EQ(GetCgroupDepth({}, "foo"), 0);
+}
+
+TEST(EmptyInputs, pathEmpty) {
+    EXPECT_EQ(GetCgroupDepth("foo", {}), 0);
+}
+
+TEST(InvalidInputs, pathNotInRoot) {
+    EXPECT_EQ(GetCgroupDepth("foo", "bar"), 0);
+}
+
+TEST(InvalidInputs, rootLargerThanPath) {
+    EXPECT_EQ(GetCgroupDepth("/a/long/path", "/short"), 0);
+}
+
+TEST(InvalidInputs, pathLargerThanRoot) {
+    EXPECT_EQ(GetCgroupDepth("/short", "/a/long/path"), 0);
+}
+
+TEST(InvalidInputs, missingSeparator) {
+    EXPECT_EQ(GetCgroupDepth("/controller/root", "/controller/rootcgroup"), 0);
+}
+
+TEST(ExtraSeparators, root) {
+    EXPECT_EQ(GetCgroupDepth("///sys/fs/cgroup", "/sys/fs/cgroup/a/b/c"), 3);
+    EXPECT_EQ(GetCgroupDepth("/sys///fs/cgroup", "/sys/fs/cgroup/a/b/c"), 3);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs///cgroup", "/sys/fs/cgroup/a/b/c"), 3);
+
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "///sys/fs/cgroup/a/b/c"), 3);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys///fs/cgroup/a/b/c"), 3);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs///cgroup/a/b/c"), 3);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup///a/b/c"), 3);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/a///b/c"), 3);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/a/b///c"), 3);
+}
+
+TEST(SeparatorEndings, rootEndsInSeparator) {
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup/", "/sys/fs/cgroup/a/b"), 2);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup///", "/sys/fs/cgroup/a/b"), 2);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup/", "/sys/fs/cgroup/a/b/"), 2);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup///", "/sys/fs/cgroup/a/b/"), 2);
+}
+
+TEST(SeparatorEndings, pathEndsInSeparator) {
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/a/b/"), 2);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/a/b///"), 2);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup/", "/sys/fs/cgroup/a/b/"), 2);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup/", "/sys/fs/cgroup/a/b///"), 2);
+}
+
+TEST(ValidInputs, rootHasZeroDepth) {
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup"), 0);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup/", "/sys/fs/cgroup"), 0);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/"), 0);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup/", "/sys/fs/cgroup/"), 0);
+}
+
+TEST(ValidInputs, atLeastDepth10) {
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/a/b/c/d/e/f/g/h/i/j"), 10);
+}
+
+TEST(ValidInputs, androidCgroupNames) {
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/system/uid_0/pid_1000"), 3);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/uid_0/pid_1000"), 2);
+
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/apps/uid_100000/pid_1000"), 3);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/uid_100000/pid_1000"), 2);
+
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/apps"), 1);
+    EXPECT_EQ(GetCgroupDepth("/sys/fs/cgroup", "/sys/fs/cgroup/system"), 1);
+}
+
+TEST(ValidInputs, androidCgroupNames_nonDefaultRoot) {
+    EXPECT_EQ(GetCgroupDepth("/custom/root", "/custom/root/system/uid_0/pid_1000"), 3);
+    EXPECT_EQ(GetCgroupDepth("/custom/root", "/custom/root/uid_0/pid_1000"), 2);
+
+    EXPECT_EQ(GetCgroupDepth("/custom/root", "/custom/root/apps/uid_100000/pid_1000"), 3);
+    EXPECT_EQ(GetCgroupDepth("/custom/root", "/custom/root/uid_100000/pid_1000"), 2);
+
+    EXPECT_EQ(GetCgroupDepth("/custom/root", "/custom/root/apps"), 1);
+    EXPECT_EQ(GetCgroupDepth("/custom/root", "/custom/root/system"), 1);
+}