using Bws.Core;
using Bws.Core.Querying;
namespace Bws.Gui.ViewModels;
///
/// The three expensive families for ONE entry, read when the details panel opens on it - UX-GUI-005.
///
/// WHY THE PANEL READS AT ALL, and the number decided it. Until 2026-09-24 the panel showed
/// "not read" beside the signature, the memory and who depends on an entry, and sent the reader to
/// turn on a column - which reads every entry on the machine, about 12 s of processor for the
/// signatures over 797 (2026-09-28). The owner's rule was that the panel reads for itself if the cost allows, and the cost was
/// measured before anything was built (tools/details-probe, 797 entries, five counted runs, each in
/// a fresh process): the three families for one typical service 46.7-66.9 ms the first time a
/// process asks, the signature family 6-13 ms every time after, and 425-518 ms for the one 98 MB
/// driver on the machine. The command line's `show` verb already read this way, over one entry.
///
/// BESIDE THE READING GUARD, NOT INSIDE IT, and that is the difference from the second phase.
/// That phase runs inside the reading and stops the tick for its seconds - the price it names at
/// its own head. This one is tens of milliseconds and must not stop the list, so it runs beside
/// the guard and is kept honest by two things instead: an answer is written only if the index is
/// still holding the listing it was read against, and it is written into the one row it is about
/// through - never through the whole-list absorb, which removes
/// every row it is not handed.
///
/// INTO THE ROW RATHER THAN ONLY INTO THE PANEL, because the panel and a copy must say the
/// same thing about the same entry - Details.AsText carries that rule. And never into
/// _have: one filled row does not answer a question about the whole list, so a query about
/// signatures still asks for the second phase exactly as before.
///
internal sealed partial class Readings : IEntryReads
{
///
/// Which whole listing the rows hold. Raised every time the index takes a whole list - a full
/// reading AND the second phase - because either replaces the entries the panel read against.
///
/// Both, and the second is the one that is easy to miss. The second phase absorbs the
/// listing with only the families somebody asked for, so with just the memory column on it
/// hands every row an entry without a signature - including the one the panel had just filled.
/// Counting only full readings would leave that signature marked as already asked for, and the
/// panel on "not read" until the next opening.
///
private int _listing;
/// What the panel has asked for, per entry, since the index last took a whole list.
private readonly Dictionary _claimed = new(StringComparer.OrdinalIgnoreCase);
///
/// Which process each claim on memory was made for - since 2026-10-27, logic report R07. A claim on
/// one process's memory is no claim on the next one's: the tick moves a row to a new process and
/// leaves its memory unread (), and a claim
/// that still stood would keep the panel from asking for it until the next whole listing.
///
private readonly Dictionary> _memoryFor = new(StringComparer.OrdinalIgnoreCase);
///
/// Why the panel's reading of an entry failed, per entry, for as long as its claim stands.
///
/// Beside the claims and cleared with them, since the review of PR #16. The failure was
/// the panel's until then, and a reading belongs to one ENTRY: opening another entry while a
/// reading was out dressed the new one in the old one's failure, and opening the failed one again
/// said nothing at all - the claim stood, so nothing was read, and the lines said "not read" as
/// though nobody had tried. Kept here, a failure is said for the entry it happened to, every time
/// that entry is shown, until a new listing lets the panel try again.
///
private readonly Dictionary _failures = new(StringComparer.OrdinalIgnoreCase);
///
public string FailureOf(EntryRow row)
{
ArgumentNullException.ThrowIfNull(row);
return _failures.GetValueOrDefault(row.ServiceName, string.Empty);
}
///
public ExtraRead Claim(EntryRow row)
{
ArgumentNullException.ThrowIfNull(row);
var entry = row.Entry;
var claimed = Claimed(row);
var missing =
(entry.Signature.Outcome == ReadOutcome.NotRead ? ExtraRead.Signatures : ExtraRead.None)
| (entry.Memory.Outcome == ReadOutcome.NotRead ? ExtraRead.Memory : ExtraRead.None)
| (entry.RequiredBy.Outcome == ReadOutcome.NotRead ? ExtraRead.RequiredBy : ExtraRead.None);
var owed = missing & Available & ~claimed;
if (owed != ExtraRead.None)
{
Stake(row, claimed | owed);
}
return owed;
}
///
/// The list holds every row's memory, and the tick has just moved rows to another process - read
/// theirs now rather than leaving "not read" in a column somebody turned on until F5 (R07, 2026-10-27).
/// The panel's own path, one row at a time: a tick moves a handful of rows at most, and a pass over
/// the listing costs 5-8 ms (MemoryPass). A row whose memory is already being read is not asked twice.
///
/// Inside the tick, awaited, the way the second phase runs inside a reading - so the list is
/// narrowed once, with the new figures, and no read is started and walked away from.
///
private Task MemoryAgainForMovedAsync()
{
if (!_have.HasFlag(ExtraRead.Memory) || _reader is null)
{
return Task.CompletedTask;
}
var reads = new List();
foreach (var row in _index.Ordered)
{
var claimed = Claimed(row);
if (row.Entry.Memory.Outcome == ReadOutcome.NotRead && !claimed.HasFlag(ExtraRead.Memory))
{
Stake(row, claimed | ExtraRead.Memory);
reads.Add(ReadAsync(row, ExtraRead.Memory));
}
}
return Task.WhenAll(reads);
}
/// What has been claimed for this row - less a claim on memory made for another process (R07).
private ExtraRead Claimed(EntryRow row)
{
var claimed = _claimed.GetValueOrDefault(row.ServiceName);
return claimed.HasFlag(ExtraRead.Memory)
&& _memoryFor.TryGetValue(row.ServiceName, out var process)
&& !(process.Outcome == row.Entry.ProcessId.Outcome && process.Value == row.Entry.ProcessId.Value)
? claimed & ~ExtraRead.Memory
: claimed;
}
private void Stake(EntryRow row, ExtraRead claimed)
{
_claimed[row.ServiceName] = claimed;
if (claimed.HasFlag(ExtraRead.Memory))
{
_memoryFor[row.ServiceName] = row.Entry.ProcessId;
}
}
///
public async Task ReadAsync(EntryRow row, ExtraRead families)
{
ArgumentNullException.ThrowIfNull(row);
var listing = _listing;
var before = row.Entry;
// THE WHOLE LISTING FOR THE MEMORY, taken here on the window's thread: the pass counts how
// many entries share each process, so over one entry it would call a shared svchost private
// - MemoryPass says so where it is written. The index is fresher than the last full reading,
// because the tick writes the process identifiers back into it.
var everything = _index.Everything;
ScmEntry read;
try
{
read = await Task.Run(() => One(before, everything, families)).ConfigureAwait(true);
}
#pragma warning disable CA1031
// Broad, for the reason the second phase gives at its own catch: every file answers for
// itself, so what arrives here is the reading failing as a whole. It must neither take the
// window down nor leave the panel implying that nobody tried - so it becomes a sentence
// about THIS entry, said by the panel whenever the entry is shown. Not for a listing that
// has gone, whose entries nobody will be shown again.
catch (Exception failure)
{
if (!_gone && listing == _listing)
{
_failures[row.ServiceName] = string.Join(" ", Causes.Of(failure));
}
return;
}
#pragma warning restore CA1031
// ANSWERED FOR ENTRIES THAT NO LONGER EXIST, or for a window nobody is looking at: dropped.
// The panel asks again for the fresh entry, because the new listing cleared what it claimed.
if (_gone || listing != _listing)
{
return;
}
var now = row.Entry;
var sameProcess = now.ProcessId.Outcome == before.ProcessId.Outcome && now.ProcessId.Value == before.ProcessId.Value;
// A RESTART BETWEEN THE QUESTION AND THE ANSWER, since the review of PR #16: the memory read
// belongs to the old process and is dropped below, and the claim goes with it so the panel
// asks again for the new one. A restart arrives through the tick, which never clears a
// claim - without this the line said "not read" until the next full reading, and Enter did
// nothing. It cannot spin: each round is a real reading, and it stops the moment the process
// holds still for the few milliseconds one takes.
if (families.HasFlag(ExtraRead.Memory) && !sameProcess && _claimed.TryGetValue(row.ServiceName, out var had))
{
_claimed[row.ServiceName] = had & ~ExtraRead.Memory;
}
_index.AbsorbOne(Carry(now, read, families, sameProcess));
}
/// Called wherever the index takes a whole list - see .
private void TookAWholeList()
{
_listing++;
_claimed.Clear();
_memoryFor.Clear();
_failures.Clear();
}
///
/// The three families for one entry, through the product's own passes and in the second phase's
/// order - files, then processes, then the manager. The same calls the command line's `show`
/// verb makes, and what tools/details-probe measured.
///
private ScmEntry One(ScmEntry entry, IReadOnlyList everything, ExtraRead families)
{
var one = entry;
if (families.HasFlag(ExtraRead.Signatures))
{
one = SecondPass.Fill([one], _inspector!)[0];
}
if (families.HasFlag(ExtraRead.Memory))
{
// Not in the listing any more - removed between the question and the answer - leaves the
// memory unread, which is true, rather than taking a figure from somewhere else.
var counted = MemoryPass.Fill(everything, _reader!)
.FirstOrDefault(candidate => string.Equals(candidate.ServiceName, one.ServiceName, StringComparison.OrdinalIgnoreCase));
one = counted is null ? one : one with { Memory = counted.Memory };
}
if (families.HasFlag(ExtraRead.RequiredBy))
{
one = RequiredByPass.Fill([one], _catalog)[0];
}
return one;
}
///
/// What was read, onto the entry the row holds NOW - which the tick may have moved since, so its
/// status and process are kept and does
/// not light the row as changed.
///
/// The memory only if the process is still the one that was asked. A service restarted
/// between the question and the answer has a new process, and the old one's figure against it
/// would be a stranger's memory in the right field.
///
private static ScmEntry Carry(ScmEntry now, ScmEntry read, ExtraRead families, bool sameProcess)
{
var carried = now;
if (families.HasFlag(ExtraRead.Signatures))
{
carried = carried with { Signature = read.Signature, FileVersion = read.FileVersion, BinaryHash = read.BinaryHash };
}
if (families.HasFlag(ExtraRead.Memory) && sameProcess)
{
carried = carried with { Memory = read.Memory };
}
if (families.HasFlag(ExtraRead.RequiredBy))
{
carried = carried with { RequiredBy = read.RequiredBy };
}
return carried;
}
}