db7833609d
- Search: the oninput setter scanned the whole Galaxy synchronously per keystroke. It now debounces 150ms, runs on the thread pool with cancellation (newer keystroke supersedes), and reads a shared singleton DashboardBrowseSearchIndex of pre-lowercased references keyed on the Galaxy cache Sequence. - Disposal: DisposeAsync awaited both loops unbounded and the loops caught only OCE. Loops now handle faults per iteration (poll faults surface in _readError, deploy-notifier faults in a retrying warning), mutate the tree on the dispatcher, drain concurrently with a 5s bound, and the CTSs are disposed in finally. - Circuits: DisconnectedCircuitRetentionPeriod is 30s (was the 3 min default) and a scoped DashboardCircuitConnectionTracker (CircuitHandler) lets the 2s ReadBulk poll skip ticks while the browser is disconnected. - Reads pass a per-page viewer id to the live-data per-viewer share.
118 lines
4.8 KiB
C#
118 lines
4.8 KiB
C#
using ZB.MOM.WW.GalaxyRepository;
|
|
using ZB.MOM.WW.GalaxyRepository.Grpc;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
|
|
|
|
/// <summary>
|
|
/// Flat, pre-lowercased attribute index behind the Browse page's filter box, shared by
|
|
/// every circuit and rebuilt only when the Galaxy cache sequence changes.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The filter used to walk every object and attribute of the cached Galaxy, doing two
|
|
/// case-insensitive <c>Contains</c> per attribute, synchronously in the property setter
|
|
/// bound to <c>oninput</c> — so a large Galaxy was rescanned on the render dispatcher
|
|
/// once per keystroke, per circuit. The page now debounces and runs the search off the
|
|
/// dispatcher; this index makes each search a single pass of ordinal substring checks
|
|
/// over strings lowercased once per Galaxy deploy rather than once per comparison.
|
|
/// Lowercasing is invariant-culture, which matches the previous
|
|
/// <see cref="StringComparison.OrdinalIgnoreCase"/> behaviour for the ASCII tag names
|
|
/// Galaxy uses.
|
|
/// </remarks>
|
|
public sealed class DashboardBrowseSearchIndex
|
|
{
|
|
// How often a scan checks its cancellation token. A superseded keystroke stops
|
|
// within this many attributes rather than finishing the whole Galaxy.
|
|
private const int CancellationCheckInterval = 1024;
|
|
|
|
private IndexSnapshot? _snapshot;
|
|
private int _buildCount;
|
|
|
|
/// <summary>Gets how many times the index has been (re)built (diagnostic, for tests).</summary>
|
|
internal int BuildCount => Volatile.Read(ref _buildCount);
|
|
|
|
/// <summary>
|
|
/// Returns up to <paramref name="limit"/> attributes whose full tag reference or
|
|
/// attribute name contains <paramref name="rawQuery"/>, case-insensitively, in
|
|
/// Galaxy object order.
|
|
/// </summary>
|
|
/// <param name="entry">The current Galaxy cache entry; its sequence keys the index.</param>
|
|
/// <param name="rawQuery">The filter text as typed; it is trimmed.</param>
|
|
/// <param name="limit">Maximum number of matches.</param>
|
|
/// <param name="cancellationToken">Cancelled when a newer query supersedes this one.</param>
|
|
/// <returns>The matches; empty for a blank query.</returns>
|
|
/// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was cancelled.</exception>
|
|
public IReadOnlyList<GalaxyAttribute> Search(
|
|
GalaxyHierarchyCacheEntry entry,
|
|
string rawQuery,
|
|
int limit,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entry);
|
|
|
|
string query = (rawQuery ?? string.Empty).Trim().ToLowerInvariant();
|
|
if (query.Length == 0 || limit <= 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
IndexedAttribute[] attributes = GetOrBuild(entry, cancellationToken);
|
|
List<GalaxyAttribute> matches = [];
|
|
for (int i = 0; i < attributes.Length; i++)
|
|
{
|
|
if (i % CancellationCheckInterval == 0)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
}
|
|
|
|
IndexedAttribute candidate = attributes[i];
|
|
if (candidate.FullTagReference.Contains(query, StringComparison.Ordinal)
|
|
|| candidate.AttributeName.Contains(query, StringComparison.Ordinal))
|
|
{
|
|
matches.Add(candidate.Attribute);
|
|
if (matches.Count >= limit)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
private IndexedAttribute[] GetOrBuild(GalaxyHierarchyCacheEntry entry, CancellationToken cancellationToken)
|
|
{
|
|
IndexSnapshot? current = Volatile.Read(ref _snapshot);
|
|
if (current is not null && current.Sequence == entry.Sequence)
|
|
{
|
|
return current.Attributes;
|
|
}
|
|
|
|
// A racing build for the same sequence is harmless: the projection is pure, so
|
|
// either winner stores equivalent content.
|
|
List<IndexedAttribute> attributes = [];
|
|
foreach (GalaxyObject galaxyObject in entry.Objects)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
foreach (GalaxyAttribute attribute in galaxyObject.Attributes)
|
|
{
|
|
attributes.Add(new IndexedAttribute(
|
|
attribute.FullTagReference.ToLowerInvariant(),
|
|
attribute.AttributeName.ToLowerInvariant(),
|
|
attribute));
|
|
}
|
|
}
|
|
|
|
IndexSnapshot built = new(entry.Sequence, [.. attributes]);
|
|
Volatile.Write(ref _snapshot, built);
|
|
Interlocked.Increment(ref _buildCount);
|
|
return built.Attributes;
|
|
}
|
|
|
|
private sealed record IndexSnapshot(long Sequence, IndexedAttribute[] Attributes);
|
|
|
|
private readonly record struct IndexedAttribute(
|
|
string FullTagReference,
|
|
string AttributeName,
|
|
GalaxyAttribute Attribute);
|
|
}
|