Skip to content
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
4 changes: 2 additions & 2 deletions ProcessManager.App/CommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal enum RunMode : byte { Desktop, Terminal, List, Find, Kill, SelfTest, He
internal sealed record CommandLineOptions {

public RunMode Mode { get; init; } = RunMode.Desktop;
public ProcessColumn SortColumn { get; init; } = ProcessColumn.CpuPercent;
public ProcessField SortColumn { get; init; } = ProcessField.CpuPercent;
public bool SortDescending { get; init; } = true;
public bool TreeMode { get; init; }

Expand Down Expand Up @@ -118,7 +118,7 @@ public static CommandLineOptions Parse(string[] args) {
case "--sort": {
if (!TryValue(args, ref i, inlineValue, out var column))
return options with { Error = "--sort needs a column" };
if (!ProcessColumnExtensions.TryParse(column, out var parsed))
if (!FieldRegistry.TryParse(column, out var parsed))
return options with { Error = $"unknown sort column '{column}'" };

options = options with { SortColumn = parsed, SortDescending = parsed.PrefersDescending() };
Expand Down
2 changes: 1 addition & 1 deletion ProcessManager.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ private static int Main(string[] args) {
);

// The view is rebuilt once per sample by both front-ends, so its cost is part of the frame.
var view = new ProcessView { TreeMode = true, SortColumn = ProcessColumn.CpuPercent };
var view = new ProcessView { TreeMode = true, SortColumn = ProcessField.CpuPercent };
view.Rebuild(sampler.Current, sampler.Delta);
var viewStart = Stopwatch.GetTimestamp();
for (var i = 0; i < 50; ++i)
Expand Down
208 changes: 208 additions & 0 deletions ProcessManager.Core/Query/FieldAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
using System.Globalization;
using Hawkynt.ProcessManager.Model;
using Hawkynt.ProcessManager.Sampling;

namespace Hawkynt.ProcessManager.Query;

/// <summary>
/// Reads one field out of a process: as text to display, as a number to compare, and as an ordering.
/// </summary>
/// <remarks>
/// The single place any field is turned into anything. Both front-ends render through
/// <see cref="Text"/>, the view sorts through <see cref="Compare"/>, and the filter compares through
/// <see cref="Number"/> — so a value reads the same in the window and in the terminal, and sorting by
/// a column can never disagree with what that column shows (PRD §5.1).
/// </remarks>
public static class FieldAccessor {

/// <summary>
/// What the field shows, including the reason when it shows no value (PRD §72.3).
/// </summary>
/// <param name="delta">
/// May be <see langword="null"/> before a second sample exists, in which case every derived field
/// reads as "not sampled yet" rather than as zero.
/// </param>
public static string Text(ProcessField field, in ProcessRecord process, SnapshotDelta? delta, int index) {
switch (field) {
case ProcessField.Name: return process.Name;
case ProcessField.Pid: return process.Pid.ToString(CultureInfo.InvariantCulture);
case ProcessField.PidHex: return "0x" + process.Pid.ToString("X", CultureInfo.InvariantCulture);
case ProcessField.ParentPid:
return process.ParentPid > 0 ? process.ParentPid.ToString(CultureInfo.InvariantCulture) : "—";
case ProcessField.UserName:
return process.UserName ?? Humanize.Placeholder(UnknownReason.NotPermitted);
case ProcessField.State: return Humanize.State(process.State);

case ProcessField.CpuPercent: return Humanize.Percent(Rated(delta, index, field));
case ProcessField.CpuPercentPerCore: return Humanize.Percent(Rated(delta, index, field));
case ProcessField.CpuTime: return Humanize.Duration(process.CpuTimeNs);
case ProcessField.CyclesDelta: return Humanize.Rate(Rated(delta, index, field));
case ProcessField.ContextSwitchesDelta: return Humanize.Rate(Rated(delta, index, field));
case ProcessField.PageFaultsDelta: return Humanize.Rate(Rated(delta, index, field));

case ProcessField.PrivateBytes: return Humanize.Bytes(process.PrivateBytes);
case ProcessField.PrivateBytesDelta: return Humanize.SignedBytesPerSecond(Rated(delta, index, field));
case ProcessField.PrivateWorkingSet: return Humanize.Bytes(process.PrivateWorkingSetBytes);
case ProcessField.WorkingSetBytes: return Humanize.Bytes(process.WorkingSetBytes);
case ProcessField.PeakWorkingSet: return Humanize.Bytes(process.PeakWorkingSetBytes);
case ProcessField.VirtualBytes: return Humanize.Bytes(process.VirtualBytes);
case ProcessField.PeakVirtualBytes: return Humanize.Bytes(process.PeakVirtualBytes);
case ProcessField.PagedPool: return Humanize.Bytes(process.PagedPoolBytes);
case ProcessField.PeakPagedPool: return Humanize.Bytes(process.PeakPagedPoolBytes);
case ProcessField.NonPagedPool: return Humanize.Bytes(process.NonPagedPoolBytes);
case ProcessField.PeakNonPagedPool: return Humanize.Bytes(process.PeakNonPagedPoolBytes);
case ProcessField.Swap: return Humanize.Bytes(process.SwapBytes);

case ProcessField.IoTotalRate:
case ProcessField.ReadBytesPerSecond:
case ProcessField.WriteBytesPerSecond:
return Humanize.BytesPerSecond(Rated(delta, index, field));

case ProcessField.ThreadCount: return process.ThreadCount.ToString(CultureInfo.InvariantCulture);
case ProcessField.HandleCount: return Humanize.Count(process.HandleCount);
case ProcessField.Priority: return process.Priority.ToString(CultureInfo.InvariantCulture);
case ProcessField.SessionId:
return process.SessionId >= 0 ? process.SessionId.ToString(CultureInfo.InvariantCulture) : "—";
case ProcessField.StartTime:
return process.StartTimeUtcTicks > 0
? new DateTime(process.StartTimeUtcTicks, DateTimeKind.Utc).ToLocalTime()
.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
: "—";
case ProcessField.Container: return process.ContainerPath ?? "—";
case ProcessField.ImagePath: return process.ImagePath ?? "—";
case ProcessField.CommandLine: return process.CommandLine ?? string.Empty;

// The graphs are drawn, not written. Asking for their text is a caller bug, but returning
// empty is friendlier than throwing in a render loop.
case ProcessField.CpuHistory:
case ProcessField.MemoryHistory:
case ProcessField.IoHistory:
default:
return string.Empty;
}
}

/// <summary>
/// The field as a plain number, for filtering and for sorting.
/// </summary>
/// <returns>
/// <see langword="null"/> when the field has no number at all — either because it is text, or
/// because this platform does not report it. A filter must treat those two the same way: a process
/// whose value is unknown does not match <c>&gt; 0</c>, and it does not match <c>== 0</c> either.
/// </returns>
public static double? Number(ProcessField field, in ProcessRecord process, SnapshotDelta? delta, int index) {
switch (field) {
case ProcessField.Pid:
case ProcessField.PidHex: return process.Pid;
case ProcessField.ParentPid: return process.ParentPid;
case ProcessField.State: return (byte)process.State;
case ProcessField.ThreadCount: return process.ThreadCount;
case ProcessField.Priority: return process.Priority;
case ProcessField.SessionId: return process.SessionId;
case ProcessField.StartTime: return process.StartTimeUtcTicks;

case ProcessField.CpuTime: return Number(process.CpuTimeNs);
case ProcessField.PrivateBytes: return Number(process.PrivateBytes);
case ProcessField.PrivateWorkingSet: return Number(process.PrivateWorkingSetBytes);
case ProcessField.WorkingSetBytes: return Number(process.WorkingSetBytes);
case ProcessField.PeakWorkingSet: return Number(process.PeakWorkingSetBytes);
case ProcessField.VirtualBytes: return Number(process.VirtualBytes);
case ProcessField.PeakVirtualBytes: return Number(process.PeakVirtualBytes);
case ProcessField.PagedPool: return Number(process.PagedPoolBytes);
case ProcessField.PeakPagedPool: return Number(process.PeakPagedPoolBytes);
case ProcessField.NonPagedPool: return Number(process.NonPagedPoolBytes);
case ProcessField.PeakNonPagedPool: return Number(process.PeakNonPagedPoolBytes);
case ProcessField.Swap: return Number(process.SwapBytes);
case ProcessField.HandleCount: return Number(process.HandleCount);

case ProcessField.CpuPercent:
case ProcessField.CpuPercentPerCore:
case ProcessField.CyclesDelta:
case ProcessField.ContextSwitchesDelta:
case ProcessField.PageFaultsDelta:
case ProcessField.PrivateBytesDelta:
case ProcessField.IoTotalRate:
case ProcessField.ReadBytesPerSecond:
case ProcessField.WriteBytesPerSecond: {
var rate = Rated(delta, index, field);
return rate.HasValue ? rate.Value : null;
}

default: return null;
}
}

/// <summary>The field as raw text, for substring and regular-expression filtering.</summary>
/// <remarks>
/// Deliberately not <see cref="Text"/>: a filter must match what the value <em>is</em>, not how it
/// was abbreviated for a column. Searching for a path should not fail because the column showed
/// an em dash, and searching "1024" should not match a cell that reads "1.0K".
/// </remarks>
public static string? RawText(ProcessField field, in ProcessRecord process) => field switch {
ProcessField.Name => process.Name,
ProcessField.UserName => process.UserName,
ProcessField.ImagePath => process.ImagePath,
ProcessField.CommandLine => process.CommandLine,
ProcessField.Container => process.ContainerPath,
ProcessField.State => Humanize.State(process.State),
ProcessField.Pid => process.Pid.ToString(CultureInfo.InvariantCulture),
ProcessField.ParentPid => process.ParentPid.ToString(CultureInfo.InvariantCulture),
_ => null,
};

/// <summary>
/// Orders two rows by one field. Text compares case-insensitively; numbers compare numerically;
/// a value that is unknown sorts below every known one, whichever direction is chosen.
/// </summary>
public static int Compare(
ProcessField field,
in ProcessRecord a,
int indexA,
in ProcessRecord b,
int indexB,
SnapshotDelta? delta
) {
switch (field) {
case ProcessField.Name:
return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
case ProcessField.UserName:
return string.Compare(a.UserName, b.UserName, StringComparison.OrdinalIgnoreCase);
case ProcessField.CommandLine:
return string.Compare(a.CommandLine, b.CommandLine, StringComparison.OrdinalIgnoreCase);
case ProcessField.ImagePath:
return string.Compare(a.ImagePath, b.ImagePath, StringComparison.OrdinalIgnoreCase);
case ProcessField.Container:
return string.Compare(a.ContainerPath, b.ContainerPath, StringComparison.OrdinalIgnoreCase);
}

var left = Number(field, in a, delta, indexA);
var right = Number(field, in b, delta, indexB);
if (left is null)
return right is null ? 0 : -1;
if (right is null)
return 1;

return left.Value.CompareTo(right.Value);
}

private static double? Number(Counter counter) => counter.HasValue ? counter.Value : null;

private static Rate Rated(SnapshotDelta? delta, int index, ProcessField field) {
if (delta is null)
return Rate.NotSampledYet;

return field switch {
ProcessField.CpuPercent => delta.CpuPercent(index),
ProcessField.CpuPercentPerCore => delta.CpuPercentPerCore(index),
ProcessField.CyclesDelta => delta.CyclesPerSecond(index),
ProcessField.ContextSwitchesDelta => delta.ContextSwitchesPerSecond(index),
ProcessField.PageFaultsDelta => delta.PageFaultsPerSecond(index),
ProcessField.PrivateBytesDelta => delta.PrivateBytesDelta(index),
ProcessField.IoTotalRate => delta.IoTotalBytesPerSecond(index),
ProcessField.ReadBytesPerSecond => delta.ReadBytesPerSecond(index),
ProcessField.WriteBytesPerSecond => delta.WriteBytesPerSecond(index),
_ => Rate.NotSampledYet,
};
}

}
Loading
Loading