Skip to content

Commit b8197bf

Browse files
authored
Merge pull request #175 from HanSur94/claude/bold-zhukovsky-0108e4
Fix all failing CI on main: concurrency smoke, MEX parity, example_dock shading, wiki links
2 parents fa45b1a + b4f953f commit b8197bf

10 files changed

Lines changed: 131 additions & 35 deletions

File tree

install.m

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,27 @@
106106
first_run(root);
107107
end
108108

109+
% --- Always: ensure the Concurrency lockfile_mex is compiled ---
110+
% lockfile_mex is the only MEX NOT committed to the repo, so the
111+
% needs_build() gate above (which probes the committed FastSense
112+
% binary_search_mex) returns false on a clean checkout that ships
113+
% prebuilt FastSense binaries — and first_run()/build_mex() never
114+
% runs build_concurrency_mex(). Build it here, independent of that
115+
% gate, so every consumer (tests, FileLock, the concurrency CI
116+
% smoke) gets it. Best-effort: honour FASTSENSE_SKIP_BUILD (CI with
117+
% cached/prebuilt binaries) and never block install() on a missing
118+
% C compiler (FileLock falls back to pure-MATLAB sidecar mode).
119+
if isempty(getenv('FASTSENSE_SKIP_BUILD')) ...
120+
&& exist('lockfile_mex', 'file') ~= 3 ...
121+
&& exist('build_concurrency_mex', 'file') == 2
122+
try
123+
build_concurrency_mex();
124+
catch concErr
125+
warning('install:concurrencyMexFailed', ...
126+
'lockfile_mex compile skipped (non-fatal): %s', concErr.message);
127+
end
128+
end
129+
109130
% --- Once per session: JIT warmup ---
110131
jit_warmup();
111132
end

libs/FastSense/FastSense.m

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,20 +1144,31 @@ function render(obj, progressBar)
11441144
S = obj.Shadings(i);
11451145
if numel(S.X) > shadingCacheSize * 2
11461146
% Build cache from raw, then render from cache
1147-
[cx, cy1] = minmax_downsample(S.X, S.Y1, shadingCacheSize);
1148-
[~, cy2] = minmax_downsample(S.X, S.Y2, shadingCacheSize);
1147+
[cx, cy1] = minmax_downsample(S.X, S.Y1, shadingCacheSize);
1148+
[~, cy2] = minmax_downsample(S.X, S.Y2, shadingCacheSize);
1149+
% minmax_downsample's tail-anchor makes the output length
1150+
% data-dependent, so Y1 and Y2 may differ in length. The
1151+
% cache is re-sliced by a single shared index range in
1152+
% updateShadings, so trim all three to a common length to
1153+
% keep that slice in bounds.
1154+
Lc = min([numel(cx), numel(cy1), numel(cy2)]);
1155+
cx = cx(1:Lc);
1156+
cy1 = cy1(1:Lc);
1157+
cy2 = cy2(1:Lc);
11491158
obj.Shadings(i).CacheX = cx;
11501159
obj.Shadings(i).CacheY1 = cy1;
11511160
obj.Shadings(i).CacheY2 = cy2;
1152-
% Downsample cache to screen resolution
1153-
[xd, y1d] = minmax_downsample(cx, cy1, obj.PixelWidth);
1154-
[~, y2d] = minmax_downsample(cx, cy2, obj.PixelWidth);
1155-
patchX = [xd, fliplr(xd)];
1161+
% Downsample cache to screen resolution. Each boundary
1162+
% keeps its own X (xd / xd2) so the closed patch polygon
1163+
% always has matching vertex counts.
1164+
[xd, y1d] = minmax_downsample(cx, cy1, obj.PixelWidth);
1165+
[xd2, y2d] = minmax_downsample(cx, cy2, obj.PixelWidth);
1166+
patchX = [xd, fliplr(xd2)];
11561167
patchY = [y1d, fliplr(y2d)];
11571168
elseif numel(S.X) > obj.MinPointsForDownsample
1158-
[xd, y1d] = minmax_downsample(S.X, S.Y1, obj.PixelWidth);
1159-
[~, y2d] = minmax_downsample(S.X, S.Y2, obj.PixelWidth);
1160-
patchX = [xd, fliplr(xd)];
1169+
[xd, y1d] = minmax_downsample(S.X, S.Y1, obj.PixelWidth);
1170+
[xd2, y2d] = minmax_downsample(S.X, S.Y2, obj.PixelWidth);
1171+
patchX = [xd, fliplr(xd2)];
11611172
patchY = [y1d, fliplr(y2d)];
11621173
else
11631174
patchX = [S.X, fliplr(S.X)];
@@ -4044,9 +4055,9 @@ function updateShadings(obj)
40444055
y2Vis = srcY2(idxStart:idxEnd);
40454056

40464057
if nVis > obj.MinPointsForDownsample
4047-
[xd, y1d] = minmax_downsample(xVis, y1Vis, pw);
4048-
[~, y2d] = minmax_downsample(xVis, y2Vis, pw);
4049-
patchX = [xd, fliplr(xd)];
4058+
[xd, y1d] = minmax_downsample(xVis, y1Vis, pw);
4059+
[xd2, y2d] = minmax_downsample(xVis, y2Vis, pw);
4060+
patchX = [xd, fliplr(xd2)];
40504061
patchY = [y1d, fliplr(y2d)];
40514062
else
40524063
patchX = [xVis, fliplr(xVis)];

tests/suite/TestAddShaded.m

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,55 @@ function testAddFillRendered(testCase)
101101
ud = get(fp.Shadings(1).hPatch, 'UserData');
102102
testCase.verifyEqual(ud.FastSense.Type, 'shaded', 'testAddFillRendered: type is shaded');
103103
end
104+
105+
function testShadedPatchEqualLengthCacheBranch(testCase)
106+
% Regression (260512-c5x): with > 2*shadingCacheSize (20000) points
107+
% and a constant fill baseline, the upper (varying) and lower
108+
% (constant) boundaries downsample to DIFFERENT lengths via the
109+
% minmax tail-anchor. render()'s cache branch must still emit an
110+
% equal-length patch polygon — previously patch() threw "Vectors
111+
% must be the same length." (e.g. example_dock's Power Systems
112+
% tile). The subsequent zoom exercises the same fix in
113+
% updateShadings(). A monotonic upper curve guarantees divergence:
114+
% its last sample IS its last bucket extreme (no tail-anchor) while
115+
% the constant baseline DOES anchor.
116+
n = 25000;
117+
x = linspace(0, 100, n);
118+
y = linspace(0, 10, n);
119+
fp = FastSense();
120+
fp.addLine(x, y, 'DisplayName', 'ramp');
121+
fp.addFill(x, y, 'Baseline', 0, 'FaceColor', [0 0.5 1]);
122+
fp.render();
123+
testCase.addTeardown(@close, fp.hFigure);
124+
125+
hP = fp.Shadings(1).hPatch;
126+
testCase.verifyEqual(numel(get(hP, 'XData')), numel(get(hP, 'YData')), ...
127+
'cache branch: shading patch XData/YData length mismatch after render');
128+
129+
% Zoom to a sub-range that stays above MinPointsForDownsample so
130+
% updateShadings re-downsamples (the live zoom/pan path).
131+
set(fp.hAxes, 'XLim', [20 60]);
132+
drawnow; pause(0.2);
133+
testCase.verifyEqual(numel(get(hP, 'XData')), numel(get(hP, 'YData')), ...
134+
'updateShadings: shading patch XData/YData length mismatch after zoom');
135+
end
136+
137+
function testShadedPatchEqualLengthMidBranch(testCase)
138+
% MinPointsForDownsample (5000) < n <= 2*shadingCacheSize (20000)
139+
% exercises render()'s mid-size (elseif) downsample branch with the
140+
% same varying-vs-constant boundary divergence.
141+
n = 10000;
142+
x = linspace(0, 100, n);
143+
y = linspace(0, 5, n);
144+
fp = FastSense();
145+
fp.addLine(x, y);
146+
fp.addFill(x, y, 'Baseline', 0);
147+
fp.render();
148+
testCase.addTeardown(@close, fp.hFigure);
149+
150+
hP = fp.Shadings(1).hPatch;
151+
testCase.verifyEqual(numel(get(hP, 'XData')), numel(get(hP, 'YData')), ...
152+
'mid branch: shading patch XData/YData length mismatch after render');
153+
end
104154
end
105155
end

tests/suite/TestMexParity.m

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,13 @@ function testBuildStoreNaNHandling(testCase)
341341
yOut(odd(~minFirst)) = yMaxVals(~minFirst);
342342
xOut(even(~minFirst)) = xMinVals(~minFirst);
343343
yOut(even(~minFirst)) = yMinVals(~minFirst);
344+
% Tail-anchor (260512-c5x): mirror minmax_core_mex.c /
345+
% minmax_downsample.m. Append (segX(end), segY(end)) iff its X
346+
% strictly exceeds the last emitted X. Length: 2*nb or 2*nb+1.
347+
if segX(end) > xOut(end)
348+
xOut(end + 1) = segX(end);
349+
yOut(end + 1) = segY(end);
350+
end
344351
end
345352

346353
function [xOut, yOut] = lttb_core_matlab(x, y, numOut)

tests/test_mex_parity.m

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,13 @@ function test_mex_parity()
197197
yOut(odd(~minFirst)) = yMaxVals(~minFirst);
198198
xOut(even(~minFirst)) = xMinVals(~minFirst);
199199
yOut(even(~minFirst)) = yMinVals(~minFirst);
200+
% Tail-anchor (260512-c5x): mirror minmax_core_mex.c / minmax_downsample.m.
201+
% Append (segX(end), segY(end)) iff its X strictly exceeds the last
202+
% emitted X. Output length: 2*nb or 2*nb+1.
203+
if segX(end) > xOut(end)
204+
xOut(end + 1) = segX(end);
205+
yOut(end + 1) = segY(end);
206+
end
200207
end
201208

202209
function [xOut, yOut] = lttb_core_matlab(x, y, numOut)

wiki/Companion-Overview.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
The FastSense Companion is a three-pane `uifigure` control panel that browses the project's `TagRegistry`, opens dashboards and ad-hoc plots in their own MATLAB figures, and provides live status monitoring across the entire project. It is purely a navigator — every dashboard it opens runs in a standalone classical figure with its own live timer, theme, and toolbar.
44

5-
Two parallel help systems live inside FastSense: **System 1** is the per-dashboard `Info` button driven by `DashboardEngine.InfoFile`, and **System 2** is this Wiki Browser. See [Dashboard Info vs Wiki](Dashboard-Info-vs-Wiki) for the full distinction.
5+
Two parallel help systems live inside FastSense: **System 1** is the per-dashboard `Info` button driven by `DashboardEngine.InfoFile`, and **System 2** is this Wiki Browser. See [Dashboard Info vs Wiki](Dashboard-Info-vs-Wiki.md) for the full distinction.
66

77
## Three-pane layout
88

@@ -16,9 +16,9 @@ The right pane is *adaptive*: when one tag is selected it shows metadata, thresh
1616

1717
## Top toolbar (left to right)
1818

19-
- **Events** — opens the [Event Viewer](Event-Viewer)
19+
- **Events** — opens the [Event Viewer](Event-Viewer.md)
2020
- **Live: ON/OFF** — toggles the companion-driven inspector refresh and the live log
21-
- **Tags** — opens the [Tag Status Table](Tag-Status-Table)
21+
- **Tags** — opens the [Tag Status Table](Tag-Status-Table.md)
2222
- **Tile / Close all** — manages the windows the Companion has opened (dashboards, ad-hoc plots, detached panes)
2323
- **Wiki** — opens this Wiki Browser (you are reading it now)
2424
- **Gear** — opens Companion settings (theme, live period)
@@ -30,7 +30,7 @@ The bottom of the window hosts two compact log panes:
3030
- **Events log** — rolling list of recent threshold violations from `EventStore`
3131
- **Live log** — per-tag `Δ samples` and latest value as new data lands
3232

33-
Each pane has a pop-out icon in its header that detaches the pane into its own figure window. See [Event Viewer](Event-Viewer) for the events pane and [Live Log](Live-Log) for the live updates pane.
33+
Each pane has a pop-out icon in its header that detaches the pane into its own figure window. See [Event Viewer](Event-Viewer.md) for the events pane and [Live Log](Live-Log.md) for the live updates pane.
3434

3535
## Opening a dashboard
3636

@@ -44,8 +44,8 @@ The **Live: ON/OFF** toggle controls a Companion-owned `timer` that drives the i
4444

4545
## See also
4646

47-
- [Tag Status Table](Tag-Status-Table)
48-
- [Event Viewer](Event-Viewer)
49-
- [Live Log](Live-Log)
50-
- [Dashboard Info vs Wiki](Dashboard-Info-vs-Wiki)
51-
- [Home](Home)
47+
- [Tag Status Table](Tag-Status-Table.md)
48+
- [Event Viewer](Event-Viewer.md)
49+
- [Live Log](Live-Log.md)
50+
- [Dashboard Info vs Wiki](Dashboard-Info-vs-Wiki.md)
51+
- [Home](Home.md)

wiki/Dashboard-Info-vs-Wiki.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,5 @@ The two systems never collide on the same button. They can both exist on the sam
5252

5353
## See also
5454

55-
- [Companion Overview](Companion-Overview)
56-
- [Home](Home)
55+
- [Companion Overview](Companion-Overview.md)
56+
- [Home](Home.md)

wiki/Event-Viewer.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ The Companion's bottom strip hosts two compact log panes:
4444
- **Events log** — rolling list of recent detected events (this surface's compact form)
4545
- **Live log** — per-tag sample-delta counts as new data arrives (a different surface)
4646

47-
See [Live Log](Live-Log) for the live updates pane.
47+
See [Live Log](Live-Log.md) for the live updates pane.
4848

4949
## See also
5050

51-
- [Live Log](Live-Log)
52-
- [Tag Status Table](Tag-Status-Table)
53-
- [Companion Overview](Companion-Overview)
51+
- [Live Log](Live-Log.md)
52+
- [Tag Status Table](Tag-Status-Table.md)
53+
- [Companion Overview](Companion-Overview.md)

wiki/Live-Log.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Buffer is capped at 500 rows, newest first. When the cap is reached the oldest r
1717

1818
## Tracking source
1919

20-
The Live Log does **not** track per-tag sample cursors itself — `FastSenseCompanion.scanLiveTagUpdates_` owns the `LiveSampleCount_` map and calls `addLiveLogEntry(tagKey, delta, latestY)` whenever a positive delta is detected. This boundary is fixed by Phase 1027 CONTEXT and is the same separation the [Event Viewer](Event-Viewer)'s events log uses — pipeline state lives in the Companion, panes only render rows.
20+
The Live Log does **not** track per-tag sample cursors itself — `FastSenseCompanion.scanLiveTagUpdates_` owns the `LiveSampleCount_` map and calls `addLiveLogEntry(tagKey, delta, latestY)` whenever a positive delta is detected. This boundary is fixed by Phase 1027 CONTEXT and is the same separation the [Event Viewer](Event-Viewer.md)'s events log uses — pipeline state lives in the Companion, panes only render rows.
2121

2222
## Filter
2323

@@ -29,14 +29,14 @@ The **Clear** button next to the filter wipes the buffer entirely.
2929

3030
Only while the Companion is in **Live mode** (top toolbar's "Live: ON"). When Live is OFF the live pipeline is idle and no new rows arrive. Existing rows stay visible.
3131

32-
The [Tag Status Table](Tag-Status-Table) is the exception — it polls under its own window-owned timer and stays current even when Live is OFF.
32+
The [Tag Status Table](Tag-Status-Table.md) is the exception — it polls under its own window-owned timer and stays current even when Live is OFF.
3333

3434
## Detached vs inline
3535

3636
When detached, the pane re-parents itself into a standalone `uifigure` and keeps its full buffer history. Closing the detached figure re-attaches the pane inline. The buffer is preserved across the round-trip.
3737

3838
## See also
3939

40-
- [Event Viewer](Event-Viewer)
41-
- [Tag Status Table](Tag-Status-Table)
42-
- [Companion Overview](Companion-Overview)
40+
- [Event Viewer](Event-Viewer.md)
41+
- [Tag Status Table](Tag-Status-Table.md)
42+
- [Companion Overview](Companion-Overview.md)

wiki/Tag-Status-Table.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,6 @@ Click again to **Resume polling**. Useful when you want a stable snapshot of the
5353

5454
## See also
5555

56-
- [Companion Overview](Companion-Overview)
57-
- [Live Log](Live-Log)
58-
- [Event Viewer](Event-Viewer)
56+
- [Companion Overview](Companion-Overview.md)
57+
- [Live Log](Live-Log.md)
58+
- [Event Viewer](Event-Viewer.md)

0 commit comments

Comments
 (0)