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

Aggregate adjacent memory sizes regardless of r+x #45401

Merged
merged 11 commits into from
Dec 10, 2020
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;

internal static partial class Interop
{
internal static partial class procfs
{
private const string MapsFileName = "/maps";

private static string GetMapsFilePathForProcess(int pid)
{
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + MapsFileName;
}

private static (long Start, int Size) TryParseAddressRange(string s, ref int start, ref int end)
{
int pos = s.IndexOf('-', start, end - start);
if (pos > 0)
{
if (long.TryParse(s.AsSpan(start, pos), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out long startingAddress) &&
long.TryParse(s.AsSpan(pos + 1, end - (pos + 1)), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out long endingAddress))
{
return (startingAddress, (int)(endingAddress - startingAddress));
}
}

return default;
}

private static bool HasReadAndExecFlags(string s, ref int start, ref int end)
{
bool sawRead = false, sawExec = false;
for (int i = start; i < end; i++)
{
if (s[i] == 'r')
sawRead = true;
else if (s[i] == 'x')
sawExec = true;
}

return sawRead & sawExec;
}

internal static ProcessModuleCollection? ParseMapsModules(int pid)
{
try
{
return ParseMapsModulesCore(File.ReadLines(GetMapsFilePathForProcess(pid)));
}
catch (IOException) { }
catch (UnauthorizedAccessException) { }

return null;
}

private static ProcessModuleCollection ParseMapsModulesCore(IEnumerable<string> lines)
{
Debug.Assert(lines != null);

ProcessModule? module = null;
ProcessModuleCollection modules = new(capacity: 0);
bool moduleHasReadAndExecFlags = false;

foreach (string line in lines)
{
// Use a StringParser to avoid string.Split costs
var parser = new StringParser(line, separator: ' ', skipEmpty: true);

// Parse the address start and size
(long Start, int Size) addressRange = parser.ParseRaw(TryParseAddressRange);

if (addressRange == default)
{
Commit();
continue;
}

// Parse the permissions
bool lineHasReadAndExecFlags = parser.ParseRaw(HasReadAndExecFlags);

// Skip past the offset, dev, and inode fields
parser.MoveNext();
parser.MoveNext();
parser.MoveNext();

// Parse the pathname
if (!parser.MoveNext())
{
Commit();
continue;
}

string pathname = parser.ExtractCurrentToEnd();
bool isContinuation = module?.FileName == pathname && (long)module.BaseAddress + module.ModuleMemorySize == addressRange.Start;

if (isContinuation)
{
module!.ModuleMemorySize += addressRange.Size;
continue;
}

Commit();
moduleHasReadAndExecFlags |= lineHasReadAndExecFlags;

module = new ProcessModule
{
FileName = pathname,
ModuleName = Path.GetFileName(pathname),
ModuleMemorySize = addressRange.Size,
EntryPointAddress = IntPtr.Zero // unknown
};

// on 32-bit platforms, it throws System.OverflowException without the void* cast.
unsafe
{
module.BaseAddress = new IntPtr(unchecked((void*)addressRange.Start));
}
}

// Commit last line.
Commit();

return modules;

void Commit()
{
// we only add module to collection, if at least one row had 'r' and 'x' set.
if (moduleHasReadAndExecFlags && module is not null)
{
modules.Add(module);
module = null;
moduleHasReadAndExecFlags = false;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
Expand All @@ -17,7 +16,6 @@ internal static partial class procfs
private const string ExeFileName = "/exe";
private const string CmdLineFileName = "/cmdline";
private const string StatFileName = "/stat";
private const string MapsFileName = "/maps";
private const string FileDescriptorDirectoryName = "/fd/";
private const string TaskDirectoryName = "/task/";

Expand Down Expand Up @@ -78,12 +76,6 @@ internal struct ParsedStat
//internal long cguest_time;
}

internal struct ParsedMapsModule
{
internal string FileName;
internal KeyValuePair<long, long> AddressRange;
}

internal static string GetExeFilePathForProcess(int pid)
{
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + ExeFileName;
Expand All @@ -99,11 +91,6 @@ internal static string GetStatFilePathForProcess(int pid)
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + StatFileName;
}

internal static string GetMapsFilePathForProcess(int pid)
{
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + MapsFileName;
}

internal static string GetTaskDirectoryPathForProcess(int pid)
{
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + TaskDirectoryName;
Expand All @@ -114,80 +101,6 @@ internal static string GetFileDescriptorDirectoryPathForProcess(int pid)
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + FileDescriptorDirectoryName;
}

internal static IEnumerable<ParsedMapsModule> ParseMapsModules(int pid)
{
try
{
return ParseMapsModulesCore(File.ReadLines(GetMapsFilePathForProcess(pid)));
}
catch (IOException) { }
catch (UnauthorizedAccessException) { }

return Array.Empty<ParsedMapsModule>();
}

private static IEnumerable<ParsedMapsModule> ParseMapsModulesCore(IEnumerable<string> lines)
{
Debug.Assert(lines != null);

// Parse each line from the maps file into a ParsedMapsModule result
foreach (string line in lines)
{
// Use a StringParser to avoid string.Split costs
var parser = new StringParser(line, separator: ' ', skipEmpty: true);

// Parse the address range
KeyValuePair<long, long> addressRange =
parser.ParseRaw(delegate (string s, ref int start, ref int end)
{
long startingAddress = 0, endingAddress = 0;
int pos = s.IndexOf('-', start, end - start);
if (pos > 0)
{
if (long.TryParse(s.AsSpan(start, pos), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out startingAddress))
{
long.TryParse(s.AsSpan(pos + 1, end - (pos + 1)), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out endingAddress);
}
}
return new KeyValuePair<long, long>(startingAddress, endingAddress);
});

// Parse the permissions (we only care about entries with 'r' and 'x' set)
if (!parser.ParseRaw(delegate (string s, ref int start, ref int end)
{
bool sawRead = false, sawExec = false;
for (int i = start; i < end; i++)
{
if (s[i] == 'r')
sawRead = true;
else if (s[i] == 'x')
sawExec = true;
}
return sawRead & sawExec;
}))
{
continue;
}

// Skip past the offset, dev, and inode fields
parser.MoveNext();
parser.MoveNext();
parser.MoveNext();

// Parse the pathname
if (!parser.MoveNext())
{
continue;
}
string pathname = parser.ExtractCurrentToEnd();

// We only get here if a we have a non-empty pathname and
// the permissions included both readability and executability.
// Yield the result.
yield return new ParsedMapsModule { FileName = pathname, AddressRange = addressRange };
}
}

private static string GetStatFilePathForThread(int pid, int tid)
{
// Perf note: Calling GetTaskDirectoryPathForProcess will allocate a string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@
Link="Common\Interop\Linux\Interop.cgroups.cs" />
<Compile Include="$(CommonPath)Interop\Linux\procfs\Interop.ProcFsStat.cs"
Link="Common\Interop\Linux\Interop.ProcFsStat.cs" />
<Compile Include="$(CommonPath)Interop\Linux\procfs\Interop.ProcFsStat.ParseMapModules.cs"
Link="Common\Interop\Linux\Interop.ProcFsStat.ParseMapModules.cs" />
<Compile Include="$(CommonPath)Interop\Linux\procfs\Interop.ProcFsStat.TryReadStatusFile.cs"
Link="Common\Interop\Linux\Interop.ProcFsStat.TryReadStatusFile.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.SchedGetSetAffinity.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,40 +38,7 @@ public static ProcessInfo[] GetProcessInfos(string machineName)
/// <returns>The array of modules.</returns>
internal static ProcessModuleCollection GetModules(int processId)
{
var modules = new ProcessModuleCollection(0);

// Process from the parsed maps file each entry representing a module
foreach (Interop.procfs.ParsedMapsModule entry in Interop.procfs.ParseMapsModules(processId))
{
int sizeOfImage = (int)(entry.AddressRange.Value - entry.AddressRange.Key);

// A single module may be split across multiple map entries; consolidate based on
// the name and address ranges of sequential entries.
if (modules.Count > 0)
{
ProcessModule module = modules[modules.Count - 1];
if (module.FileName == entry.FileName &&
((long)module.BaseAddress + module.ModuleMemorySize == entry.AddressRange.Key))
{
// Merge this entry with the previous one
module.ModuleMemorySize += sizeOfImage;
continue;
}
}

// It's not a continuation of a previous entry but a new one: add it.
unsafe
{
modules.Add(new ProcessModule()
{
FileName = entry.FileName,
ModuleName = Path.GetFileName(entry.FileName),
BaseAddress = new IntPtr(unchecked((void*)entry.AddressRange.Key)),
ModuleMemorySize = sizeOfImage,
EntryPointAddress = IntPtr.Zero // unknown
});
}
}
ProcessModuleCollection modules = Interop.procfs.ParseMapsModules(processId) ?? new(capacity: 0);

// Move the main executable module to be the first in the list if it's not already
string? exePath = Process.GetExePath(processId);
Expand Down