ContainerHelper.h#

Fully qualified name: omni/extras/ContainerHelper.h

File members: omni/extras/ContainerHelper.h

// Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//
#pragma once

#include "../../carb/Defines.h"

#if CARB_PLATFORM_LINUX
#    include <fcntl.h>
#    include <unistd.h>

#    include <string>
#    include <utility>
#    include <vector>
#endif

namespace omni
{
namespace extras
{

#if CARB_PLATFORM_LINUX || defined(DOXYGEN_BUILD)

#    ifndef DOXYGEN_SHOULD_SKIP_THIS
namespace detail
{

inline bool readLineFromFile(const char* file, char* buffer, size_t len) noexcept
{
    auto fd = ::open(file, O_RDONLY, 0);
    if (fd == -1)
    {
        return false;
    }

    auto size = CARB_RETRY_EINTR(::read(fd, buffer, len - 1));
    ::close(fd);

    if (size <= 0)
    {
        return false;
    }

    buffer[size] = '\0';
    return true;
}

inline int32_t readIntFromFile(const char* file) noexcept
{
    char buffer[64];

    if (!readLineFromFile(file, buffer, CARB_COUNTOF(buffer)))
    {
        return -1;
    }

    return std::atoi(buffer);
}

struct CpuQuota
{
    int32_t quota = -1;

    int32_t period = 100'000;
};

inline CpuQuota readCpuQuotaFromFile(const char* file) noexcept
{
    char buffer[64];
    int quota;
    int period;
    char* endp = nullptr;

    if (!readLineFromFile(file, buffer, CARB_COUNTOF(buffer)))
    {
        return { -1, -1 };
    }

    // attempt to parse two values from the line.  The first value may be 'max' or a number
    // between 0 and 100000.  The second number is often 100000 but could be different depending
    // on scheduler settings.  We expect this file to always be properly formatted.

    // set to use the maximum quota for all available CPU cores => fail.
    if (strncmp(buffer, "max ", 4) == 0)
    {
        return { -1, std::atoi(&buffer[4]) };
    }

    quota = (int)strtol(buffer, &endp, 10);
    period = std::atoi(endp);

    return { quota, period };
}

inline int32_t getCpuSetCoreCount(const char* buffer, std::vector<int32_t>* cores = nullptr) noexcept
{
    std::string str = buffer;
    size_t pos = 0;
    size_t next;
    int32_t coreStart;
    int32_t coreEnd;
    int32_t total = 0;

    while (1)
    {
        std::string start;
        std::string end;

        next = str.find_first_of(",-", pos);

        // last item in the list (must be a single core) or a single core => parse and add it.
        if (next == std::string::npos || str[next] == ',')
        {
            size_t limit = (next == std::string::npos) ? str.length() : next;

            start = str.substr(pos, limit - pos);
            coreStart = std::atoi(start.c_str());
            total++;

            if (cores != nullptr)
            {
                cores->push_back(coreStart);
            }

            // done
            if (next == std::string::npos)
            {
                break;
            }
        }

        // found a core range => parse the start and end and add them.
        else if (str[next] == '-')
        {
            size_t posEnd = next + 1;

            start = str.substr(pos, next - pos);
            coreStart = std::atoi(start.c_str());

            next = str.find_first_of(",", posEnd);
            end = str.substr(posEnd, next - posEnd);
            coreEnd = std::atoi(end.c_str());

            // add the total core count.  Note that the ranges are always inclusive so we always
            // need to add 1 to the calculated range.
            total += coreEnd - coreStart + 1;

            // add all the cores to the list (inclusive on both ends of the range).
            if (cores != nullptr)
            {
                for (int32_t i = coreStart; i <= coreEnd; i++)
                {
                    cores->push_back(i);
                }
            }

            // nothing left in the source string => done.
            if (next == std::string::npos)
            {
                break;
            }
        }

        pos = next + 1;
    }

    return total;
}

inline int readCpuSetCoreCountFromFile(const char* file, char* buffer = nullptr, size_t len = 0) noexcept
{
    char localBuffer[1024];

    if (buffer == nullptr || len == 0)
    {
        buffer = localBuffer;
        len = CARB_COUNTOF(localBuffer);
    }

    // read the first line from the file.  This will contain the CPU core list.  This list is
    // comma separated where each value may either be a single core index or a core index range.
    // For example, "0-1,3" would indicate that cores 0, 1, and 3 are available and "0-3,7,9"
    // would indicate that cores 0, 1, 2, 3, 7, and 9 are available.
    if (!readLineFromFile(file, buffer, len))
    {
        return -1;
    }

    // return the total number of cores included in the CPU set.
    return getCpuSetCoreCount(buffer);
}

inline bool isRunningInContainer() noexcept
{
    FILE* fp;

    // first (and easiest) check is to check whether the `/.dockerenv` file exists.  This file
    // is not necessarily always present though.
    if (access("/.dockerenv", F_OK) == 0)
    {
        return true;
    }

    // a more reliable but more expensive check is to verify the control group of `init`.  If
    // running under docker, all of the entries will have a path that starts with `/docker` or
    // `/lxc` instead of just `/`.
    // Kubernetes seems to use `:/kubepods.slice`.
    fp = fopen("/proc/1/cgroup", "r");

    if (fp != nullptr)
    {
        char line[256];

        while (fgets(line, CARB_COUNTOF(line), fp) != nullptr)
        {
            if (feof(fp) || ferror(fp))
                break;

            if (strstr(line, ":/docker") != nullptr || strstr(line, ":/lxc") != nullptr ||
                strstr(line, ":/kubepods") != nullptr)
            {
                return true;
            }
        }

        fclose(fp);
    }

    return false;
}

inline bool getCgroupCpuQuota(CpuQuota& quota)
{
    // attempt to read the CPU quota from cgroup v2 first.
    quota = readCpuQuotaFromFile("/sys/fs/cgroup/cpu.max");

    if (quota.quota > 0 && quota.period > 0)
    {
        return true;
    }

    // attempt to read the CPU quota from cgroup v1 next.
    quota.quota = detail::readIntFromFile("/sys/fs/cgroup/cpu/cpu.cfs_quota_us");
    quota.period = detail::readIntFromFile("/sys/fs/cgroup/cpu/cpu.cfs_period_us");

    if (quota.quota > 0 && quota.period > 0)
    {
        return true;
    }

    // CPU quota not available.
    return false;
}

inline int32_t getCgroupCpuQuota() noexcept
{
    CpuQuota quota;

    // attempt to read the CPU quota from cgroup v2 and cgroup v1.  This can affect how much
    // of the assigned cores scheduled time the processes in the container are effectively allowed
    // to make use of.
    if (getCgroupCpuQuota(quota))
    {
        return ::carb_max(1, (quota.quota + (quota.period / 2)) / quota.period);
    }

    return -1;
}

inline int getDockerCpuLimit() noexcept
{
    // See:
    // https://docs.docker.com/config/containers/resource_constraints/#cpu
    // https://engineering.squarespace.com/blog/2017/understanding-linux-container-scheduling
    // https://docs.kernel.org/admin-guide/cgroup-v1/cpusets.html
    // https://docs.kernel.org/admin-guide/cgroup-v2.html

    // ****** read the CPU core limit if defined ******
    // first attempt to read from the cgroup v2 and v1 CPU set lists to get the total core count.
    // We'll try the 'effective' CPU set list first.  If they doesn't exist or is empty, we'll fall
    // back to the main CPU set lists.  Note that the v1 and v2 cgroups will never simultaneously
    // exist in the container and that Docker will always respect the host system's cgroup version
    // regardless of the container's base image version.
    int32_t coreCount;

    // try to read the cgroup v2 effective CPU set.
    coreCount = detail::readCpuSetCoreCountFromFile("/sys/fs/cgroup/cpuset.cpus.effective");

    // fall back to the cgroup v2 main CPU set.
    if (coreCount < 0)
        coreCount = detail::readCpuSetCoreCountFromFile("/sys/fs/cgroup/cpuset.cpus");

    // fall back to the cgroup v1 effective CPU set.
    if (coreCount < 0)
        coreCount = detail::readCpuSetCoreCountFromFile("/sys/fs/cgroup/cpuset/cpuset.effective_cpus");

    // fall back to the cgroup v1 main CPU set.
    if (coreCount < 0)
        coreCount = detail::readCpuSetCoreCountFromFile("/sys/fs/cgroup/cpuset/cpuset.cpus");

    // ****** read the CPU usage quota if defined ******
    // next attempt to read the CPU quota from cgroup v2 and cgroup v1.  This can affect how much
    // of the assigned cores scheduled time the processes in the container are effectively allowed
    // to make use of.
    int32_t coreQuota = getCgroupCpuQuota();

    // ****** calculate the effective CPU core limit ******
    // a CPU set has been assigned and has limited the core count for the container => calculate
    //   the effective core count from the CPU set and the CPU quota.
    if (coreCount > 0)
    {
        // a CPU quota has been defined as well => caclulate the effective core count from both
        //   values.  Note that the CPU usage quota can oversubscribe the assigned core count.
        //   In that case we'd want to return the minimum of the CPU set core count and the
        //   effective count from the CPU quota.
        if (coreQuota > 0)
            return ::carb_min(coreQuota, coreCount);

        // no CPU usage quota defined => just return the CPU set core count.
        return coreCount;
    }

    // no CPU set core count defined => just return the effective core count from the CPU quota.
    else if (coreQuota > 0)
    {
        return coreQuota;
    }

    // no CPU usage limit.  This will effectively limit it to the host's bare metal CPU core
    // count.
    return -1;
}

} // namespace detail
#    endif

inline int getDockerCpuLimit() noexcept
{
    static int s_coreCount = detail::getDockerCpuLimit();
    return s_coreCount;
}

inline bool isRunningInContainer()
{
    static bool s_inContainer = detail::isRunningInContainer();
    return s_inContainer;
}

#else

inline int getDockerCpuLimit() noexcept
{
    return -1;
}

inline bool isRunningInContainer() noexcept
{
    return false;
}

#endif

} // namespace extras
} // namespace omni