Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Change Process.MaxWorkingSet to prefer cgroup data #35645

Merged
merged 1 commit into from
Mar 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions src/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Buffers.Text;
using System.Diagnostics;
using System.IO;

internal static partial class Interop
{
internal static partial class cgroups
{
/// <summary>Path to mountinfo file in procfs for the current process.</summary>
private const string ProcMountInfoFilePath = "/proc/self/mountinfo";
/// <summary>Path to cgroup directory in procfs for the current process.</summary>
private const string ProcCGroupFilePath = "/proc/self/cgroup";

/// <summary>Path to the found cgroup location, or null if it couldn't be found.</summary>
internal static readonly string s_cgroupMemoryPath = FindCGroupPath("memory");
/// <summary>Path to the found cgroup memory limit_in_bytes path, or null if it couldn't be found.</summary>
private static readonly string s_cgroupMemoryLimitPath = s_cgroupMemoryPath != null ? s_cgroupMemoryPath + "/memory.limit_in_bytes" : null;

/// <summary>Tries to read the memory limit from the cgroup memory location.</summary>
/// <param name="limit">The read limit, or 0 if it couldn't be read.</param>
/// <returns>true if the limit was read successfully; otherwise, false.</returns>
public static bool TryGetMemoryLimit(out ulong limit)
{
string path = s_cgroupMemoryLimitPath;

if (path != null &&
TryReadMemoryValueFromFile(path, out limit))
{
return true;
}

limit = 0;
return false;
}

/// <summary>Tries to parse a memory limit from the specified file.</summary>
/// <param name="path">The path to the file to parse.</param>
/// <param name="result">The parsed result, or 0 if it couldn't be parsed.</param>
/// <returns>true if the value was read successfully; otherwise, false.</returns>
private static bool TryReadMemoryValueFromFile(string path, out ulong result)
{
if (File.Exists(path))
{
try
{
byte[] bytes = File.ReadAllBytes(path);
if (Utf8Parser.TryParse(bytes, out ulong ulongValue, out int bytesConsumed))
{
// If we successfully parsed the number, see if there's a K, M, or G
// multiplier value immediately following.
ulong multiplier = 1;
if (bytesConsumed < bytes.Length)
{
switch (bytes[bytesConsumed])
{

case (byte)'k':
case (byte)'K':
multiplier = 1024;
break;

case (byte)'m':
case (byte)'M':
multiplier = 1024 * 1024;
break;

case (byte)'g':
case (byte)'G':
multiplier = 1024 * 1024 * 1024;
break;
}
}

result = checked(ulongValue * multiplier);
return true;
}
}
catch (Exception e)
{
Debug.Fail($"Failed to read \"{path}\": {e}");
}
}

result = 0;
return false;
}

/// <summary>Find the cgroup path for the specified subsystem.</summary>
/// <param name="subsystem">The subsystem, e.g. "memory".</param>
/// <returns>The cgroup path if found; otherwise, null.</returns>
private static string FindCGroupPath(string subsystem)
{
if (TryFindHierarchyMount(subsystem, out string hierarchyRoot, out string hierarchyMount) &&
TryFindCGroupPathForSubsystem(subsystem, out string cgroupPathRelativeToMount))
{
// For a host cgroup, we need to append the relative path.
// In a docker container, the root and relative path are the same and we don't need to append.
return (hierarchyRoot != cgroupPathRelativeToMount) ?
hierarchyMount + cgroupPathRelativeToMount :
hierarchyMount;
}

return null;
}

/// <summary>Find the cgroup mount information for the specified subsystem.</summary>
/// <param name="subsystem">The subsystem, e.g. "memory".</param>
/// <param name="root">The path of the directory in the filesystem which forms the root of this mount; null if not found.</param>
/// <param name="path">The path of the mount point relative to the process's root directory; null if not found.</param>
/// <returns>true if the mount was found; otherwise, null.</returns>
private static bool TryFindHierarchyMount(string subsystem, out string root, out string path)
{
if (File.Exists(ProcMountInfoFilePath))
{
try
{
using (var reader = new StreamReader(ProcMountInfoFilePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
// Look for an entry that has cgroup as the "filesystem type"
// and that has options containing the specified subsystem.
// See man page for /proc/[pid]/mountinfo for details, e.g.:
// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
// but (7) is optional and could exist as multiple fields; the (8) separator marks
// the end of the optional values.

const string Separator = " - ";
int endOfOptionalFields = line.IndexOf(Separator);
if (endOfOptionalFields == -1)
{
// Malformed line.
continue;
}

string postSeparatorLine = line.Substring(endOfOptionalFields + Separator.Length);
string[] postSeparatorlineParts = postSeparatorLine.Split(' ');
if (postSeparatorlineParts.Length < 3)
{
// Malformed line.
continue;
}

if (postSeparatorlineParts[0] != "cgroup" ||
Array.IndexOf(postSeparatorlineParts[2].Split(','), subsystem) < 0)
{
// Not the relevant entry.
continue;
}

// Found the relevant entry. Extract the mount root and path.
string[] lineParts = line.Substring(0, endOfOptionalFields).Split(' ');
root = lineParts[3];
path = lineParts[4];
return true;
}
}
}
catch (Exception e)
{
Debug.Fail($"Failed to read or parse \"{ProcMountInfoFilePath}\": {e}");
}
}

root = null;
path = null;
return false;
}

/// <summary>Find the cgroup relative path for the specified subsystem.</summary>
/// <param name="subsystem">The subsystem, e.g. "memory".</param>
/// <param name="path">The found path, or null if it couldn't be found.</param>
/// <returns></returns>
private static bool TryFindCGroupPathForSubsystem(string subsystem, out string path)
{
if (File.Exists(ProcCGroupFilePath))
{
try
{
using (var reader = new StreamReader(ProcCGroupFilePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
// Find the first entry that has the subsystem listed in its controller
// list. See man page for cgroups for /proc/[pid]/cgroups format, e.g:
// hierarchy-ID:controller-list:cgroup-path
// 5:cpuacct,cpu,cpuset:/daemons

string[] lineParts = line.Split(':');
if (lineParts.Length != 3)
{
// Malformed line.
continue;
}

if (Array.IndexOf(lineParts[1].Split(','), subsystem) < 0)
{
// Not the relevant entry.
continue;
}

path = lineParts[2];
return true;
}
}
}
catch (Exception e)
{
Debug.Fail($"Failed to read or parse \"{ProcMountInfoFilePath}\": {e}");
}
}

path = null;
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,9 @@
<Compile Include="System\Diagnostics\Process.Linux.cs" />
<Compile Include="System\Diagnostics\ProcessManager.Linux.cs" />
<Compile Include="System\Diagnostics\ProcessThread.Linux.cs" />
<Compile Include="$(CommonPath)\Interop\Linux\cgroups\Interop.cgroups.cs">
<Link>Common\Interop\Linux\Interop.cgroups.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Linux\procfs\Interop.ProcFsStat.cs">
<Link>Common\Interop\Linux\Interop.ProcFsStat.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,14 @@ private unsafe IntPtr ProcessorAffinityCore
private void GetWorkingSetLimits(out IntPtr minWorkingSet, out IntPtr maxWorkingSet)
{
minWorkingSet = IntPtr.Zero; // no defined limit available
ulong rsslim = GetStat().rsslim;

// For max working set, try to respect container limits by reading
// from cgroup, but if it's unavailable, fall back to reading from procfs.
EnsureState(State.HaveNonExitedId);
if (!Interop.cgroups.TryGetMemoryLimit(out ulong rsslim))
{
rsslim = GetStat().rsslim;
}

// rsslim is a ulong, but maxWorkingSet is an IntPtr, so we need to cap rsslim
// at the max size of IntPtr. This often happens when there is no configured
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using Xunit;

namespace System.Runtime.InteropServices.RuntimeInformationTests
Expand All @@ -13,24 +15,24 @@ public class DescriptionNameTests
[Fact]
public void DumpRuntimeInformationToConsole()
{
// Not really a test, but useful to dump to the log to
// sanity check that the test run or CI job
// was actually run on the OS that it claims to be on
// Not really a test, but useful to dump a variety of information to the test log to help
// debug environmental issues, in particular in CI

string dvs = PlatformDetection.GetDistroVersionString();
string osd = RuntimeInformation.OSDescription.Trim();
string osv = Environment.OSVersion.ToString();
string osa = RuntimeInformation.OSArchitecture.ToString();
string pra = RuntimeInformation.ProcessArchitecture.ToString();
string frd = RuntimeInformation.FrameworkDescription.Trim();
Console.WriteLine($"### OS: Distro={dvs} Description={osd} Version={osv} Arch={osa}");

string lcr = PlatformDetection.LibcRelease;
string lcv = PlatformDetection.LibcVersion;

Console.WriteLine($@"### CONFIGURATION: {dvs} OS={osd} OSVer={osv} OSArch={osa} Arch={pra} Framework={frd} LibcRelease={lcr} LibcVersion={lcv}");
Console.WriteLine($"### LIBC: Release={lcr} Version={lcv}");

Console.WriteLine($"### FRAMEWORK: Version={Environment.Version} Description={RuntimeInformation.FrameworkDescription.Trim()}");

if (!PlatformDetection.IsNetNative)
{
string binariesLocation = Path.GetDirectoryName(typeof(object).Assembly.Location);
Console.WriteLine("location: " + binariesLocation);
string binariesLocationFormat = PlatformDetection.IsInAppContainer ? "Unknown" : new DriveInfo(binariesLocation).DriveFormat;
Console.WriteLine($"### BINARIES: {binariesLocation} (drive format {binariesLocationFormat})");
}
Expand All @@ -40,6 +42,100 @@ public void DumpRuntimeInformationToConsole()
Console.WriteLine($"### TEMP PATH: {tempPathLocation} (drive format {tempPathLocationFormat})");

Console.WriteLine($"### CURRENT DIRECTORY: {Environment.CurrentDirectory}");

string cgroupsLocation = Interop.cgroups.s_cgroupMemoryPath;
if (cgroupsLocation != null)
{
Console.WriteLine($"### CGROUPS MEMORY: {cgroupsLocation}");
}

Console.WriteLine($"### ENVIRONMENT VARIABLES");
foreach (DictionaryEntry envvar in Environment.GetEnvironmentVariables())
{
Console.WriteLine($"###\t{envvar.Key}: {envvar.Value}");
}

using (Process p = Process.GetCurrentProcess())
{
var sb = new StringBuilder();
sb.AppendLine("### PROCESS INFORMATION:");
sb.AppendFormat($"###\tArchitecture: {RuntimeInformation.ProcessArchitecture.ToString()}").AppendLine();
foreach (string prop in new string[]
{
#pragma warning disable 0618 // some of these Int32-returning properties are marked obsolete
nameof(p.BasePriority),
nameof(p.HandleCount),
nameof(p.Id),
nameof(p.MachineName),
nameof(p.MainModule),
nameof(p.MainWindowHandle),
nameof(p.MainWindowTitle),
nameof(p.MaxWorkingSet),
nameof(p.MinWorkingSet),
nameof(p.NonpagedSystemMemorySize),
nameof(p.NonpagedSystemMemorySize64),
nameof(p.PagedMemorySize),
nameof(p.PagedMemorySize64),
nameof(p.PagedSystemMemorySize),
nameof(p.PagedSystemMemorySize64),
nameof(p.PeakPagedMemorySize),
nameof(p.PeakPagedMemorySize64),
nameof(p.PeakVirtualMemorySize),
nameof(p.PeakVirtualMemorySize64),
nameof(p.PeakWorkingSet),
nameof(p.PeakWorkingSet64),
nameof(p.PriorityBoostEnabled),
nameof(p.PriorityClass),
nameof(p.PrivateMemorySize),
nameof(p.PrivateMemorySize64),
nameof(p.PrivilegedProcessorTime),
nameof(p.ProcessName),
nameof(p.ProcessorAffinity),
nameof(p.Responding),
nameof(p.SessionId),
nameof(p.StartTime),
nameof(p.TotalProcessorTime),
nameof(p.UserProcessorTime),
nameof(p.VirtualMemorySize),
nameof(p.VirtualMemorySize64),
nameof(p.WorkingSet),
nameof(p.WorkingSet64),
#pragma warning restore 0618
})
{
sb.Append($"###\t{prop}: ");
try
{
sb.Append(p.GetType().GetProperty(prop).GetValue(p));
}
catch (Exception e)
{
sb.Append($"(Exception: {e.Message})");
}
sb.AppendLine();
}
Console.WriteLine(sb.ToString());
}

if (osd.Contains("Linux"))
{
// Dump several procfs files
foreach (string path in new string[] { "/proc/self/mountinfo", "/proc/self/cgroup", "/proc/self/limits" })
{
Console.WriteLine($"### CONTENTS OF \"{path}\":");
try
{
using (Process cat = Process.Start("cat", path))
{
cat.WaitForExit();
}
}
catch (Exception e)
{
Console.WriteLine($"###\t(Exception: {e.Message})");
}
}
}
}

[Fact]
Expand Down
Loading