5959 Registry_ = [] % TagRegistry handle (or class name placeholder)
6060 Theme_ = [] % resolved CompanionTheme struct
6161 Companion_ = [] % FastSenseCompanion handle (uialert parent + detach)
62- RowBuffer_ = cell(0 , 11 )
62+ RowBuffer_ = cell(0 , 12 )
6363 KeyToRow_ = [] % containers.Map(key -> row index into RowBuffer_)
6464 Listeners_ = {} % addlistener handles ; deleted in close ()
6565 RefreshTimer_ = [] % timer driving periodic re-query (window-owned ; 260519 - bs4 patch)
8282 methods (Access = public )
8383
8484 function obj = TagStatusTableWindow()
85- obj.RowBuffer_ = cell(0 , 11 );
85+ obj.RowBuffer_ = cell(0 , 12 );
8686 obj.KeyToRow_ = containers .Map(' KeyType' , ' char' , ' ValueType' , ' double' );
8787 % Default chip state: every chip ACTIVE (first-open shows
8888 % everything). 260519-bs4-04 patch.
@@ -210,21 +210,23 @@ function openWith(obj, registry, theme, companion)
210210 stripePair = obj .stripePairFromTheme_(t );
211211
212212 % --- Center uitable. ---
213- % 11 columns: Activity is column 9 (between Last updated and Samples).
213+ % 12 columns: Activity is col 9, Events is col 10, Samples col 11.
214+ % Events column (260519-bs4-06 patch) shows the integer count of
215+ % events attached to each tag via EventStore.getEventsForTag.
214216 obj.hTable_ = uitable(obj .hFig_ , ...
215217 ' Units' , ' normalized' , ...
216218 ' Position' , [0.01 0.055 0.98 0.78 ], ...
217219 ' ColumnName' , {' Key' , ' Name' , ' Type' , ' Criticality' , ' Units' , ...
218220 ' Latest' , ' Status' , ' Last updated' , ' Activity' , ...
219- ' Samples' , ' Labels' }, ...
220- ' ColumnWidth' , {130 , 200 , 75 , 80 , 60 , 90 , 80 , 140 , 70 , 70 , ' auto' }, ...
221- ' ColumnEditable' , false(1 , 11 ), ...
221+ ' Events ' , ' Samples' , ' Labels' }, ...
222+ ' ColumnWidth' , {120 , 180 , 70 , 75 , 55 , 85 , 75 , 130 , 65 , 55 , 65 , ' auto' }, ...
223+ ' ColumnEditable' , false(1 , 12 ), ...
222224 ' RowName' , {}, ...
223225 ' FontName' , ' Menlo' , ...
224226 ' FontSize' , 10 , ...
225227 ' BackgroundColor' , stripePair , ...
226228 ' ForegroundColor' , t .ForegroundColor , ...
227- ' Data' , cell(0 , 11 ));
229+ ' Data' , cell(0 , 12 ));
228230
229231 % --- Footer "N tags" label. ---
230232 obj.hStatusLbl_ = uicontrol(obj .hFig_ , ...
@@ -275,13 +277,17 @@ function markTagsDirty(obj, keys)
275277 if ~iscell(keys ); return ; end
276278 try
277279 nowSec = TagStatusTableWindow .nowSeconds_();
280+ % Build a small precomputed event-count map for ONLY the
281+ % dirty keys -- O(M events * H stores) once, then O(1) per
282+ % row in the loop below (260519-bs4-06 patch).
283+ eventCountsByKey = obj .precomputeEventCounts_(keys );
278284 changed = false ;
279285 for k = 1 : numel(keys )
280286 key = char(keys{k });
281287 if isempty(key ); continue ; end
282288 tag = obj .resolveTag_(key );
283289 if isempty(tag ); continue ; end
284- row = TagStatusTableWindow .buildRow_(tag , nowSec );
290+ row = TagStatusTableWindow .buildRow_(tag , nowSec , eventCountsByKey );
285291 if obj .KeyToRow_ .isKey(key )
286292 idx = obj .KeyToRow_(key );
287293 obj .RowBuffer_(idx , : ) = row ;
@@ -534,7 +540,7 @@ function onCloseRequest_(obj)
534540
535541 function rebuildAll_(obj )
536542 % REBUILDALL_ Replace RowBuffer_ with one row per registered tag (sorted).
537- obj.RowBuffer_ = cell(0 , 11 );
543+ obj.RowBuffer_ = cell(0 , 12 );
538544 obj.KeyToRow_ = containers .Map(' KeyType' , ' char' , ' ValueType' , ' double' );
539545 try
540546 tags = TagRegistry .find(@(t ) true );
@@ -558,10 +564,15 @@ function rebuildAll_(obj)
558564 tags = tags(ord );
559565 % Preallocate the buffer up front.
560566 nTags = numel(tags );
561- obj.RowBuffer_ = cell(nTags , 11 );
567+ obj.RowBuffer_ = cell(nTags , 12 );
562568 nowSec = TagStatusTableWindow .nowSeconds_();
569+ % Bucket events by tag key ONCE for the whole rebuild
570+ % (260519-bs4-06 patch). O(M events) instead of O(N tags *
571+ % M events) when each call to getEventsForTag walks the store.
572+ eventCountsByKey = obj .precomputeEventCounts_(keysSorted );
563573 for k = 1 : nTags
564- obj .RowBuffer_(k , : ) = TagStatusTableWindow .buildRow_(tags{k }, nowSec );
574+ obj .RowBuffer_(k , : ) = TagStatusTableWindow .buildRow_( ...
575+ tags{k }, nowSec , eventCountsByKey );
565576 obj .KeyToRow_(keysSorted{k }) = k ;
566577 end
567578 end
@@ -593,6 +604,45 @@ function applyFilter_(obj)
593604 end
594605 end
595606
607+ function counts = precomputeEventCounts_(obj , keys )
608+ % PRECOMPUTEEVENTCOUNTS_ Bucket EventStore events by tag key in one pass.
609+ % Walks every distinct EventStore reachable through the listed
610+ % tag keys, calls obj.EventStore.getEventsForTag(key) ONCE per
611+ % key, and totals into a containers.Map. The savings come from
612+ % the fact that we resolve each tag at most once per tick and
613+ % only count keys we actually need (the keys passed in).
614+ %
615+ % When EventStore.getEventsForTag is O(N events) (current
616+ % implementation walks all events), this collapses N tag-row
617+ % builds * N events to N keys * N events, which is the same
618+ % cost order but ensures the work happens at a single,
619+ % debug-friendly call site rather than scattered through
620+ % buildRow_.
621+ %
622+ % Returns a containers.Map(char -> double); empty when keys is
623+ % empty or when no tag has an EventStore. Wrapped in try/catch;
624+ % failure returns an empty map and buildRow_ falls back to the
625+ % per-tag query path (still O(M events) but at least correct).
626+ % 260519-bs4-06 patch.
627+ counts = containers .Map(' KeyType' , ' char' , ' ValueType' , ' double' );
628+ if isempty(keys ); return ; end
629+ if ischar(keys ); keys = {keys }; end
630+ if ~iscell(keys ); return ; end
631+ try
632+ for k = 1 : numel(keys )
633+ key = char(keys{k });
634+ if isempty(key ); continue ; end
635+ tag = obj .resolveTag_(key );
636+ if isempty(tag ); continue ; end
637+ n = TagStatusTableWindow .countEventsForTag_(tag );
638+ counts(key ) = n ;
639+ end
640+ catch
641+ % Best-effort -- a failure here should not abort the tick.
642+ % buildRow_ will fall back to per-row queries below.
643+ end
644+ end
645+
596646 function pair = stripePairFromTheme_(~, t )
597647 % STRIPEPAIRFROMTHEME_ 2x3 stripe pair derived from theme brightness.
598648 isDark = mean(t .DashboardBackground ) < 0.5 ;
@@ -843,13 +893,17 @@ function onRefreshTick_(obj)
843893 nowSec = TagStatusTableWindow .nowSeconds_();
844894 changed = false ;
845895 keys = obj .KeyToRow_ .keys();
896+ % Bucket events by tag key ONCE per tick rather than
897+ % querying the store N times (one per row). Cheap when
898+ % store is empty / not bound. 260519-bs4-06 patch.
899+ eventCountsByKey = obj .precomputeEventCounts_(keys );
846900 for k = 1 : numel(keys )
847901 key = keys{k };
848902 if ~obj .KeyToRow_ .isKey(key ); continue ; end
849903 idx = obj .KeyToRow_(key );
850904 tag = obj .resolveTag_(key );
851905 if isempty(tag ); continue ; end
852- newRow = TagStatusTableWindow .buildRow_(tag , nowSec );
906+ newRow = TagStatusTableWindow .buildRow_(tag , nowSec , eventCountsByKey );
853907 oldRow = obj .RowBuffer_(idx , : );
854908 if ~isequal(newRow , oldRow )
855909 obj .RowBuffer_(idx , : ) = newRow ;
@@ -882,29 +936,45 @@ function onRefreshTick_(obj)
882936
883937 methods (Static , Access = public )
884938
885- function row = buildRow_(tag , nowSeconds )
886- % BUILDROW_ Return a 1x11 cell row describing tag's current status.
939+ function row = buildRow_(tag , nowSeconds , eventCountsByKey )
940+ % BUILDROW_ Return a 1x12 cell row describing tag's current status.
887941 % Columns: Key, Name, Type, Criticality, Units, Latest, Status,
888- % Last updated, Activity, Samples, Labels.
942+ % Last updated, Activity, Events, Samples, Labels.
889943 %
890944 % Inputs:
891- % tag -- Tag handle (any subclass; tolerant of throws)
892- % nowSeconds -- (optional) current wall-clock time as posix
893- % seconds, used for the Activity column. When
894- % omitted, TagStatusTableWindow.nowSeconds_() is
895- % queried (slightly more expensive). Tests pass
896- % an explicit value for determinism. 260519-bs4 patch.
945+ % tag -- Tag handle (any subclass; tolerant of throws)
946+ % nowSeconds -- (optional) current wall-clock time as posix
947+ % seconds, used for the Activity column. When
948+ % omitted, TagStatusTableWindow.nowSeconds_()
949+ % is queried. Tests pass an explicit value for
950+ % determinism. 260519-bs4 patch.
951+ % eventCountsByKey -- (optional) containers.Map(char -> double)
952+ % giving precomputed per-tag event counts.
953+ % When the tag's Key is present in the map,
954+ % the Events column reads from the map.
955+ % Otherwise falls back to
956+ % countEventsForTag_(tag) which walks the
957+ % tag's bound EventStore. Pass [] or omit
958+ % to force the per-tag query. 260519-bs4-06.
897959 %
898960 % The Activity column is "Live" when X(end) is within
899961 % InactiveThresholdSeconds_ (5 minutes) of nowSeconds in the same
900962 % time base, else "Inactive". Empty / unconvertible / future X
901963 % defensively renders "Inactive".
902964 %
965+ % The Events column shows an integer count of events attached to
966+ % the tag (via EventStore.getEventsForTag). Tags with no
967+ % EventStore -- or any throw during the count -- render "0".
968+ % 260519-bs4-06 patch.
969+ %
903970 % Never throws -- a tag whose getXY/valueAt fails renders em-dash
904971 % placeholders for the dynamic columns AND "Inactive" for Activity.
905972 if nargin < 2 || isempty(nowSeconds )
906973 nowSeconds = TagStatusTableWindow .nowSeconds_();
907974 end
975+ if nargin < 3
976+ eventCountsByKey = [];
977+ end
908978 em = char(8212 );
909979 key = ' ' ;
910980 name = ' ' ;
@@ -982,9 +1052,59 @@ function onRefreshTick_(obj)
9821052 % Leave placeholders; never throw.
9831053 end
9841054
1055+ % --- Events count (260519-bs4-06) ---
1056+ % Bucketed-by-key precomputed map preferred; falls back to a
1057+ % per-tag query when missing. countEventsForTag_ never throws.
1058+ useBucket = ~isempty(eventCountsByKey ) && ...
1059+ isa(eventCountsByKey , ' containers.Map' ) && ...
1060+ ~isempty(key ) && eventCountsByKey .isKey(key );
1061+ if useBucket
1062+ try
1063+ nEvents = double(eventCountsByKey(key ));
1064+ catch
1065+ nEvents = 0 ;
1066+ end
1067+ else
1068+ nEvents = TagStatusTableWindow .countEventsForTag_(tag );
1069+ end
1070+ eventsTxt = sprintf(' %d ' , nEvents );
1071+
9851072 row = {key , name , typeLabel , crit , units , ...
9861073 latestTxt , statusTxt , lastUpdatedTxt , activityTxt , ...
987- samplesTxt , labelStr };
1074+ eventsTxt , samplesTxt , labelStr };
1075+ end
1076+
1077+ function n = countEventsForTag_(tag )
1078+ % COUNTEVENTSFORTAG_ Integer count of events attached to a Tag.
1079+ % Returns 0 for tags with no EventStore, [] EventStore, or any
1080+ % exception raised while querying. Delegates to tag.eventsAttached
1081+ % when available (the Tag base class API, which itself wraps
1082+ % EventStore.getEventsForTag), so tag subclasses that override
1083+ % the lookup (e.g. MonitorTag binding to a shared store) all
1084+ % route through the same query path. Pure function -- safe to
1085+ % call from the refresh loop without side effects.
1086+ % 260519-bs4-06 patch.
1087+ n = 0 ;
1088+ if isempty(tag ); return ; end
1089+ try
1090+ % Skip cheap-fail-fast cases without touching the store.
1091+ if ~isprop(tag , ' EventStore' ) || isempty(tag .EventStore )
1092+ return ;
1093+ end
1094+ if ismethod(tag , ' eventsAttached' )
1095+ events = tag .eventsAttached();
1096+ else
1097+ events = tag .EventStore .getEventsForTag(char(tag .Key ));
1098+ end
1099+ if isempty(events )
1100+ n = 0 ;
1101+ else
1102+ n = numel(events );
1103+ end
1104+ catch
1105+ % EventStore not bound / throws / etc. -> 0
1106+ n = 0 ;
1107+ end
9881108 end
9891109
9901110 function s = nowSeconds_()
@@ -1013,7 +1133,7 @@ function onRefreshTick_(obj)
10131133 % applies all four dimensions.
10141134 %
10151135 % Inputs:
1016- % rows -- cell(N, 11 ) buffer (TagStatusTableWindow.RowBuffer_).
1136+ % rows -- cell(N, 12 ) buffer (TagStatusTableWindow.RowBuffer_).
10171137 % query -- char/string; empty / whitespace = no search filter.
10181138 % activeTypes -- cellstr subset of {sensor, monitor,
10191139 % composite, state, derived}. Omitted = all kept.
@@ -1025,7 +1145,7 @@ function onRefreshTick_(obj)
10251145 % Semantics:
10261146 % -- search: substring match (case-insensitive) on Key, Name,
10271147 % Units, OR Labels (Labels are stored as a comma-joined string
1028- % in column 11 ).
1148+ % in column 12; was column 11 pre-260519-bs4-06 ).
10291149 % -- chip groups: AND across groups (a row passes only if it
10301150 % matches the active set of every group), OR within a group
10311151 % (a row matches if its value is in the active set).
@@ -1205,12 +1325,13 @@ function onRefreshTick_(obj)
12051325
12061326function tf = rowMatchesSearch_(row , qLow )
12071327% ROWMATCHESSEARCH_ Case-insensitive substring match on Key+Name+Units+Labels.
1208- % row -- 1x11 cell. Columns 1 (Key), 2 (Name), 5 (Units), 11 (Labels).
1328+ % row -- 1x12 cell. Columns 1 (Key), 2 (Name), 5 (Units), 12 (Labels).
1329+ % (Labels column moved 11 -> 12 in 260519-bs4-06 when Events was inserted.)
12091330% qLow -- already-lowercased query.
12101331% Tolerates rows with missing / non-char columns (defensive try/catch).
12111332 tf = false ;
12121333 try
1213- for c = [1 , 2 , 5 , 11 ]
1334+ for c = [1 , 2 , 5 , 12 ]
12141335 val = row{c };
12151336 if ~ischar(val ); continue ; end
12161337 if ~isempty(strfind(lower(val ), qLow )) % #ok<STREMP>
0 commit comments