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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ is not part of this repository.

## [Unreleased]

### Changed

- A minimised window no longer reads the list of services every second. It reads it again the
moment it is restored, so the list is current as soon as it is on screen. A window started
minimised, from a shortcut set to "Run: Minimized", reads it once and then waits the same way.

## [0.3.0] - 2026-09-25

### Added
Expand Down
13 changes: 8 additions & 5 deletions src/Bws.Cli/CommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ internal sealed partial record CommandLine
/// <summary>
/// Read who signed each binary, which the listing does not do by default.
///
/// Measured at 4620-7656 ms over 810 entries and 544 distinct files, against 476-551 ms
/// for the rest of a listing, so it is asked for rather than assumed. A query about
/// signatures turns it on by itself - somebody who wrote signed:no has already asked.
/// Measured 2026-09-28 at about 12 s of processor over 797 entries and 531 distinct files -
/// 1.0-1.2 s of clock on sixteen processors, 4.3-5.0 s on two - against 136-144 ms for the
/// cold reading of the manager every command makes. (It was 4620-7656 ms on 2026-08-01, one
/// file at a time.) So it is asked for rather than assumed. A query about signatures turns it
/// on by itself - somebody who wrote signed:no has already asked.
///
/// The spread is wider than most whole operations here, which is itself the point: one
/// run of this tells nobody anything.
Expand Down Expand Up @@ -106,8 +108,9 @@ internal sealed partial record CommandLine
/// <summary>
/// Whether the listing should go and read who depends on each entry.
///
/// Asked for rather than always read: a call per entry, measured at 236-259 ms over 313
/// services on 2026-09-05, against 423-500 ms for the whole listing. A query naming the
/// Asked for rather than always read: a call per entry, measured 2026-09-28 at 148-155 ms over
/// 797 entries, against 107-113 ms for the whole reading on sixteen processors - more than the
/// listing it sits behind. A query naming the
/// field turns it on by itself, exactly as one about signatures or memory does.
/// </summary>
internal bool RequiredBy { get; private init; }
Expand Down
6 changes: 3 additions & 3 deletions src/Bws.Cli/OptionSurface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ internal static readonly (string Option, CommandKind[] Verbs)[] Surface =

// Listing only. A plan never asks who signed anything, so accepting it on a write
// verb would be a switch that does nothing - the silence this table was built to
// end. Measured at 4620-7656 ms over 810 entries and 544 files, which is why it is
// asked for rather than assumed.
// end. It costs about 12 s of processor over 797 entries (2026-09-28, 1.0-1.2 s of clock on
// sixteen processors), which is why it is asked for rather than assumed.
("--signatures", [CommandKind.List]),

// Listing only, for the same reason: a plan is about what will happen to a service,
Expand All @@ -153,7 +153,7 @@ internal static readonly (string Option, CommandKind[] Verbs)[] Surface =

// Listing only, and NOT the same switch as --dependents further down. That one is an
// instruction to a write verb - take the services standing on this one with you. This is
// a reading, and it costs a call per entry: 236-259 ms over 313 services.
// a reading, and it costs a call per entry: 148-155 ms over 797 entries, 2026-09-28.
("--required-by", [CommandKind.List]),

// The two verbs that resolve a launch path against the disk. A plan does not - it
Expand Down
18 changes: 11 additions & 7 deletions src/Bws.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@
// Comparing two files never opens the service control manager, and it matters that it
// does not. A pipeline step comparing two snapshots on a build agent has no business
// needing rights over that agent's own services, and reading eight hundred entries this
// branch never looks at would spend half a second saying nothing.
// branch never looks at would spend a cold reading of the manager saying nothing - 136-144 ms
// here and 215-234 ms on an eight-processor machine, measured 2026-09-28.
var offline = options.Kind == CommandKind.SnapshotDiff && !options.Live;

// Skip unless somebody said otherwise, everywhere, including the branches that never see
Expand All @@ -94,8 +95,9 @@
var read = offline ? 0 : stopwatch.ElapsedMilliseconds;

// Its own number, and not folded into the time spent filtering. The second pass is by
// far the most expensive thing this tool does - measured at around five seconds against
// a third of one for the read - and reporting it under the word "filtered" would put a
// far the most expensive thing this tool does - 869-954 ms against 136-144 ms for the read on
// sixteen processors, and 1451-1591 ms against 215-234 ms on eight, both measured 2026-09-28
// through --timing - and reporting it under the word "filtered" would put a
// true number next to a sentence about something else.
long inspected = 0;

Expand Down Expand Up @@ -188,7 +190,8 @@
// nothing to preview that the file itself does not already say.
//
// Refused before the expensive work rather than after it, and the order is the whole
// courtesy: verifying signatures takes about a second, and spending it to then say
// courtesy: verifying signatures takes about a second here and half as much again on an
// eight-processor machine (2026-09-28), and spending it to then say
// "there is already a file there" would be a second nobody got anything for.
//
// Found 2026-08-02 by reading a security document from another project, and confirmed
Expand Down Expand Up @@ -386,9 +389,10 @@
else
{
// The second pass, and the first thing in this tool that is asked for rather than
// simply done. Measured at 4620-7656 ms over 810 entries and 544 distinct files
// against 476-551 ms for everything above, so a listing does not verify signatures
// unless somebody wants them.
// simply done. Measured 2026-09-28 at about 12 s of processor over 797 entries and 531
// distinct files - 1.0-1.2 s of clock on sixteen processors and 4.3-5.0 s on two - against
// 136-144 ms for the reading above, so a listing does not verify signatures unless
// somebody wants them.
//
// A query about them counts as wanting them. Answering "signed:no" with an empty
// list because nobody had looked would be a correct query returning what reads
Expand Down
6 changes: 4 additions & 2 deletions src/Bws.Cli/Refusals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ namespace Bws.Cli;
///
/// <b>Not one of these opens a handle or reads an entry</b>, which is the property that makes the
/// whole block worth having in front of the expensive work. Refusing a typo after eight hundred
/// entries have been read is half a second spent to say "you made a typo".
/// entries have been read is a whole cold reading spent to say "you made a typo" - 136-144 ms here
/// and 215-234 ms on an eight-processor machine, measured 2026-09-28.
/// </summary>
internal static class Refusals
{
Expand Down Expand Up @@ -178,7 +179,8 @@ internal static class Refusals
// WERE ASKED AFTER THE MACHINE HAD BEEN READ. That is the one property this whole class
// exists for: nothing here opens a handle, so a typo is answered before eight hundred
// entries are enumerated. Both of these commands read the manager first and then said
// "you left out an argument" - about half a second spent to report a missing word.
// "you left out an argument" - a whole cold reading of the manager spent to report a
// missing word.
//
// Nothing about the answers changed. Same two sentences, same code, and the order they are
// asked in is the order they stood in.
Expand Down
6 changes: 3 additions & 3 deletions src/Bws.Core/IBinaryInspector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ namespace Bws.Core;
/// need nothing the manager can grant or refuse. Something that only lists services should
/// not have to be handed an object that can open and hash every binary on the machine.
///
/// It is also the seam ADR-13 needs. The second pass is expensive - measured at 4620-7656
/// ms over 810 entries and 544 distinct files, against 476-551 ms for everything the
/// listing does today - so it has to be possible to build a listing without one of these
/// It is also the seam ADR-13 needs. The second pass is expensive - about 12 s of processor
/// over 797 entries and 531 distinct files on 2026-09-28, against 107-113 ms of clock for
/// everything else the listing does - so it has to be possible to build a listing without one of these
/// at all.
/// </summary>
public interface IBinaryInspector
Expand Down
3 changes: 2 additions & 1 deletion src/Bws.Core/MemoryPass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ namespace Bws.Core;
///
/// Its own pass rather than part of the listing, and the reason is not cost - measured on
/// 2026-08-01, the whole pass costs 5-8 ms over 810 entries and 110 processes, beside the
/// 476-551 ms the listing already spends. The calls themselves are under a millisecond and
/// 476-551 ms the listing spent then (107-113 ms over 797 entries on 2026-09-28, since it reads
/// several entries at once). The calls themselves are under a millisecond and
/// the rest is building 810 new records, which is worth separating: the probe measured only
/// the calls and the figure it gave was wrong by a factor of six.
///
Expand Down
5 changes: 3 additions & 2 deletions src/Bws.Core/Planning/BulkPlanBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ public BulkPlan Build(BulkAction action)
/// not the order, which was harmless, but the question asked to work it out:
/// <see cref="DependentsFirst.Order"/> spends one <c>ReadDependents</c> per name, and each of
/// those opens the manager and then the service. Selecting three hundred entries and asking to
/// disable them paid three hundred round trips to the manager, on the thread drawing the
/// window, to sort a list whose order did not matter.
/// disable them paid three hundred round trips to the manager - on the thread drawing the
/// window, as working out a plan then was, until 2026-09-03 - to sort a list whose order did
/// not matter.
///
/// <b>A setting that carries a stop goes back to the ordered side (2026-09-24).</b> Then each
/// plan does take its entry down, and two selected entries where one depends on the other have
Expand Down
9 changes: 8 additions & 1 deletion src/Bws.Core/Planning/PlanRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ public sealed class PlanRunner(IScmControl control, IClock clock)
/// Our choice, not the system's, and it carries no correctness: the deadline comes from
/// the entry's own wait hint, this only decides how soon we notice. Short because
/// somebody is watching a terminal and most stops are over in well under a second, and
/// asking costs one call to the manager.
/// asking is cheap - five calls to the manager (open it, open the entry, close the manager,
/// query, close the entry - WindowsScmControl.Read), not the one call this said until
/// 2026-09-28.
///
/// <b>Measured on the throwaway machine on 2026-09-28, this cadence is most of every wait:</b>
/// Spooler and W32Time changed state in 2-50 ms and each step reported 260-289 ms, because the
/// first question goes straight after the request, is almost always too early, and the next
/// one is a whole cadence later. Backlog 466, S-6 of the external performance report.
/// </summary>
private static readonly TimeSpan Cadence = TimeSpan.FromMilliseconds(250);

Expand Down
81 changes: 81 additions & 0 deletions src/Bws.Core/Querying/QueryAsTyped.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
namespace Bws.Core.Querying;

/// <summary>
/// A query as it stands in the box, parsed once and then asked as often as anybody likes.
///
/// <b>Why it exists - G-4 of the external performance report of 2026-09-28.</b> One keystroke in the
/// window's search box used to parse the same line eighteen times: once to narrow the list, once to
/// ask whether it names what the scope leaves out, and once for each of the sixteen filter chips
/// asking whether it is lit. Each parse of a line holding a pattern builds a new regular expression,
/// and a pattern the engine refuses throws and is caught on every one of them (S-10 of the same
/// report). Everything that asks about one text now asks this.
///
/// <b>It is the TEXT and the answer together, never the answer alone</b>, and that is what keeps
/// it from being the second copy of the query the filter chips are forbidden to hold - the rule is
/// at FilterChip in the window. Whoever keeps one of these keeps it next to the text it was made
/// from and replaces it the moment the text differs, so it cannot disagree with the box.
///
/// <b>Parsed as being typed</b> (<see cref="QueryInput.BeingTyped"/>), because the box is the one
/// place a line is read while it is still being written. The command line reads a finished query
/// and has no use for this.
///
/// <b>Not safe to share between threads</b> - the members of a line that does not parse are worked
/// out on the first question and kept. The window asks from its own thread only.
/// </summary>
public sealed class QueryAsTyped
{
private IReadOnlyList<Query?>? _members;

private QueryAsTyped(string text, QueryParseResult parsed)
{
Text = text;
Parsed = parsed;
}

/// <summary>The text, exactly as it was typed - an absent text is an empty one.</summary>
public string Text { get; }

/// <summary>The whole line parsed, once.</summary>
public QueryParseResult Parsed { get; }

/// <summary>Parses the text once.</summary>
public static QueryAsTyped Of(string? text) =>
new(text ?? string.Empty, QueryParser.Parse(text, QueryInput.BeingTyped));

/// <summary>
/// Whether the text already carries this member, on this side.
///
/// <b>MEMBER BY MEMBER WHEN THE WHOLE LINE DOES NOT PARSE - UX-GUI-002, 2026-09-23.</b> Until
/// then a mistake anywhere in the line meant no member counted, so in
/// <c>status:running pid:abc</c> the chip for Running went dark over its own member, and
/// clicking it wrote a second copy on the end. The whole line is still asked first, because
/// that is the reading that folds repeated fields - and a line that cannot be taken apart at
/// all, an unclosed quote, carries nothing, for the reason <see cref="QueryMembers.Without"/>
/// gives. The members are parsed once for every question, not once per question.
/// </summary>
public bool Carries(string field, string value, bool negated)
{
if (Parsed.IsValid)
{
return Parsed.Query!.Carries(field, value, negated);
}

return Members().Any(member => member?.Carries(field, value, negated) ?? false);
}

private IReadOnlyList<Query?> Members() => _members ??= Scanned(Text);

/// <summary>
/// Each member of the line on its own, parsed rather than compared as text - which is the whole
/// of why quoting cannot fool a chip (the note at the top of <see cref="QueryMembers"/>).
/// </summary>
private static IReadOnlyList<Query?> Scanned(string text)
{
if (text.Length == 0 || !QueryScanner.TryScan(text, out _, out var spans, out _))
{
return [];
}

return [.. spans.Select(span => QueryParser.Parse(text[span], QueryInput.BeingTyped).Query)];
}
}
8 changes: 4 additions & 4 deletions src/Bws.Core/Querying/QueryField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,18 @@ public enum ExtraRead
{
None = 0,

/// <summary>Who signed each binary. Measured at 4620-7656 ms over 810 entries.</summary>
/// <summary>Who signed each binary. About 12 s of processor over 797 entries, measured 2026-09-28.</summary>
Signatures = 1,

/// <summary>What each running process is using. Measured at under a millisecond over 110 processes.</summary>
Memory = 2,

/// <summary>
/// Who breaks if each entry stops. Measured at 236-259 ms over 313 services on 2026-09-05.
/// Who breaks if each entry stops. Measured at 148-155 ms over 797 entries on 2026-09-28.
///
/// <b>The third family, and the one that shows why this was flags rather than a yes-or-no from
/// the start.</b> It sits between the other two - two hundred and fifty milliseconds against
/// under one and against seven and a half seconds - so a query about dependents must not send
/// the start.</b> It sits between the other two - a hundred and fifty milliseconds against
/// under one and against twelve seconds of processor - so a query about dependents must not send
/// the window to open eight hundred binaries, and a question about signatures must not walk the
/// manager service by service. Each caller asks for what it needs and gets only that, which
/// <c>Readings.Fill</c> honours one flag at a time.
Expand Down
4 changes: 2 additions & 2 deletions src/Bws.Core/Querying/QueryFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,8 @@ private static QueryField[] BuildAll() =>
// and the one services.msc answers only by opening a service and reading a tab.
//
// IT DECLARES A FAMILY AND THE ONE ABOVE DOES NOT, which is the whole difference
// between the two directions. This takes a call per entry: measured 236-259 ms over
// 313 services on 2026-09-05, against 423-500 ms for the entire listing. So it is
// between the two directions. This takes a call per entry: measured 148-155 ms over
// 797 entries on 2026-09-28, against 107-113 ms for the entire listing. So it is
// asked for rather than always read, exactly as signatures and memory are.
//
// THE FIRST HOP ONLY, which a person reading a member has to know: dependents:spooler
Expand Down
29 changes: 5 additions & 24 deletions src/Bws.Core/Querying/QueryMembers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,31 +127,12 @@ public static string Without(string? text, string field, string value, bool nega
}

/// <summary>
/// Whether the text already carries this member, on this side.
///
/// <b>MEMBER BY MEMBER WHEN THE WHOLE LINE DOES NOT PARSE - UX-GUI-002, 2026-09-23.</b> Until
/// then a mistake anywhere in the line meant no member counted, so in
/// <c>status:running pid:abc</c> the chip for Running went dark over its own member, and
/// clicking it wrote a second copy on the end. The whole line is still asked first, because
/// that is the reading that folds repeated fields - and a line that cannot be taken apart at
/// all, an unclosed quote, carries nothing, for the reason <see cref="Without"/> gives.
/// Whether the text already carries this member, on this side - for one question about one
/// text. Anything asking several questions of the same text keeps a <see cref="QueryAsTyped"/>
/// instead, which is where the answer and its argument live.
/// </summary>
public static bool Carries(string? text, string field, string value, bool negated)
{
var parsed = QueryParser.Parse(text, QueryInput.BeingTyped);

if (parsed.IsValid)
{
return parsed.Query!.Carries(field, value, negated);
}

if (string.IsNullOrEmpty(text) || !QueryScanner.TryScan(text, out _, out var spans, out _))
{
return false;
}

return spans.Exists(span => Is(text[span], field, value, negated));
}
public static bool Carries(string? text, string field, string value, bool negated) =>
QueryAsTyped.Of(text).Carries(field, value, negated);

/// <summary>How this member is written when a chip puts it there.</summary>
public static string Member(string field, string value, bool negated) =>
Expand Down
Loading
Loading