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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ The filter popup a column header opens now shows which comparison it is about to

## [1.36.0] — 2026-10-02

A file too large to show whole now lists every entry it has and loads a row's contents when that row is opened, where before it hung with no table and no message. Reported against an 8 MB `allMidi.mat` — a cell array of MIDI songs — which "keeps spinning" and never opens: it flattens to 2,016,325 nodes over 7 levels, a row was built for every one of them, and the resulting `setRows` payload was 704 MiB of JSON. V8 cannot produce a string that long (the limit is 536,870,888), so `JSON.stringify` threw inside VS Code's own `postMessage`, which is `async` and serializes the message itself — the throw therefore arrived as a rejected promise that the `try`/`catch` around the post could not see, nothing posted `setRows`, nothing posted an error, and the table's spinner ended on nothing else. Three layers, because there were three faults. The post is now awaited and a rejection becomes the error banner, so an undeliverable payload can never again be silent. A hard cap of 100,000 rows sits in front of the read-only row payload — not a tuning knob but what keeps the payload serializable at all, sized for the worst row shape rather than the measured 366 B, and taken as a PREFIX because every builder emits a parent before its children and a prefix is the only cut in which every surviving row's parent is also present; a sample would leave orphans the webview cannot place. The editable `.sldd` views get the guard but not the cap, since a structural edit computed against a truncated row set is worse than an honest banner. And then the cap alone, which is where this version's real work is: a prefix of a 2-million-row pre-order traversal is the first cell's descendants and nothing else, so ten of the eleven top-level variables were not on screen and no gesture could reach them — the file opened and was still unusable. The rule now is breadth over depth, for every data source and every entry type: stop expanding large entries, but list all entries, because a name present matters more than a value shown. The payload is planned by whole LEVELS of the tree, level by level while the next one fits, and the first level that does not is omitted entirely rather than half-delivered — which is what makes a deferred row mean exactly "has children, none of them here" and makes one fetch the whole answer for that row. The reported file now delivers 238 rows, levels 1 through 4 complete, and paints them in 7 ms. One plan spans every section rather than one plan per section, and that is the half the rule actually turns on: a per-section plan would also deliver whole levels and also defer honestly while spending the budget on the depth of section 1 and leaving section 9's entries unnamed, so a dictionary's References heading would read as empty on a file whose Design Data is deep. Opening a deferred row fetches it: the twisty is driven by the deferred mark as well as by delivered children — through one predicate read by the click, the Space key and `aria-expanded` alike, so a tree a mouse can open is one a keyboard can open — the click asks the host, the host answers from the same planner that built the payload, and the rows are merged under their parent instead of being routed through a fresh payload, which would re-derive the columns and close an open grid view. Pairing the two halves on one choice of planner is deliberate: a fetch answered by a different rule than the payload was built with is a row that opens onto the wrong thing, with no error anywhere. The answer carries a count of children that got no row at all, for the one loss a fetch can still inflict — a node with more direct children than a whole delivery holds, since a cell's children are built uncapped — and the table says that number in the banner strip, because the file's own banner was composed before the user opened anything and cannot. Entry-scoped repaints, which is how an edit writes back, take the same budget: a 1000x1000 double is 1,000,001 rows from a single name, enough to be the whole problem by itself. That banner is also now in the strip rather than in the document's normal flow ahead of a full-bleed `position:absolute` table, which had been painting over it — an error message that was in the DOM and on screen nowhere, leaving the panel reading "No data". Qualified against the reported file in a real browser over the shipped bundle: all eleven variables named, the banner legible in both themes, descending to a frontier row costing no round trip until the frontier itself, and opening a 1x19252 struct array four levels down posting one request, answering in 90 ms and landing its rows under the row that was clicked. Unchanged by any of this: parsing that file still costs about 2.3 GiB of extension-host heap, so a file several times larger exhausts the host before a single row is built — that is the MAT reader in `data-explorer-core`, and no amount of planning on this side recovers it.
A file too large to show whole now lists every entry it has and loads a row's contents when that row is opened, where before it hung with no table and no message. Reported against an 8 MB MAT-file — a cell array of MIDI songs — which "keeps spinning" and never opens: it flattens to 2,016,325 nodes over 7 levels, a row was built for every one of them, and the resulting `setRows` payload was 704 MiB of JSON. V8 cannot produce a string that long (the limit is 536,870,888), so `JSON.stringify` threw inside VS Code's own `postMessage`, which is `async` and serializes the message itself — the throw therefore arrived as a rejected promise that the `try`/`catch` around the post could not see, nothing posted `setRows`, nothing posted an error, and the table's spinner ended on nothing else. Three layers, because there were three faults. The post is now awaited and a rejection becomes the error banner, so an undeliverable payload can never again be silent. A hard cap of 100,000 rows sits in front of the read-only row payload — not a tuning knob but what keeps the payload serializable at all, sized for the worst row shape rather than the measured 366 B, and taken as a PREFIX because every builder emits a parent before its children and a prefix is the only cut in which every surviving row's parent is also present; a sample would leave orphans the webview cannot place. The editable `.sldd` views get the guard but not the cap, since a structural edit computed against a truncated row set is worse than an honest banner. And then the cap alone, which is where this version's real work is: a prefix of a 2-million-row pre-order traversal is the first cell's descendants and nothing else, so ten of the eleven top-level variables were not on screen and no gesture could reach them — the file opened and was still unusable. The rule now is breadth over depth, for every data source and every entry type: stop expanding large entries, but list all entries, because a name present matters more than a value shown. The payload is planned by whole LEVELS of the tree, level by level while the next one fits, and the first level that does not is omitted entirely rather than half-delivered — which is what makes a deferred row mean exactly "has children, none of them here" and makes one fetch the whole answer for that row. The reported file now delivers 238 rows, levels 1 through 4 complete, and paints them in 7 ms. One plan spans every section rather than one plan per section, and that is the half the rule actually turns on: a per-section plan would also deliver whole levels and also defer honestly while spending the budget on the depth of section 1 and leaving section 9's entries unnamed, so a dictionary's References heading would read as empty on a file whose Design Data is deep. Opening a deferred row fetches it: the twisty is driven by the deferred mark as well as by delivered children — through one predicate read by the click, the Space key and `aria-expanded` alike, so a tree a mouse can open is one a keyboard can open — the click asks the host, the host answers from the same planner that built the payload, and the rows are merged under their parent instead of being routed through a fresh payload, which would re-derive the columns and close an open grid view. Pairing the two halves on one choice of planner is deliberate: a fetch answered by a different rule than the payload was built with is a row that opens onto the wrong thing, with no error anywhere. The answer carries a count of children that got no row at all, for the one loss a fetch can still inflict — a node with more direct children than a whole delivery holds, since a cell's children are built uncapped — and the table says that number in the banner strip, because the file's own banner was composed before the user opened anything and cannot. Entry-scoped repaints, which is how an edit writes back, take the same budget: a 1000x1000 double is 1,000,001 rows from a single name, enough to be the whole problem by itself. That banner is also now in the strip rather than in the document's normal flow ahead of a full-bleed `position:absolute` table, which had been painting over it — an error message that was in the DOM and on screen nowhere, leaving the panel reading "No data". Qualified against the reported file in a real browser over the shipped bundle: all eleven variables named, the banner legible in both themes, descending to a frontier row costing no round trip until the frontier itself, and opening a 1x19252 struct array four levels down posting one request, answering in 90 ms and landing its rows under the row that was clicked. Unchanged by any of this: parsing that file still costs about 2.3 GiB of extension-host heap, so a file several times larger exhausts the host before a single row is built — that is the MAT reader in `data-explorer-core`, and no amount of planning on this side recovers it.

## [1.35.0] — 2026-10-02

Expand Down
10 changes: 5 additions & 5 deletions src/host/nameExtract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ export function namesFromMat(names: readonly string[], sourceUri: string): NameR
// graph concern, not a name one).
//
// One record per BLOCK, keyed by core's `blockKey` — the SID — and never by the name.
// A name is unique only inside its own system: f14.slx holds four blocks named `Gain`
// in four subsystems, which deduped by name gave ONE search hit that could reveal only
// whichever row came first, and the Constant whose label the file leaves blank had no
// hit at all (an empty name was dropped). Both are now searchable, told apart by their
// path, and revealed by the key the row publishes as `_blockKey`.
// A name is unique only inside its own system: a shipped Simulink demo model has four
// `Gain` blocks in four subsystems, which deduped by name gave ONE search hit that could
// reveal only whichever row came first, and the Constant whose label the file leaves
// blank had no hit at all (an empty name was dropped). Both are now searchable, told
// apart by their path, and revealed by the key the row publishes as `_blockKey`.
//
// A block with neither a name nor a SID has no key, and is still dropped: nothing could
// be searched for, and nothing could be selected if it were found.
Expand Down
23 changes: 12 additions & 11 deletions src/host/usageCells.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export interface RawSource {
// and a target that navigates back to it.
//
// The model is part of the payload and not an embellishment: two models can hold
// blocks of the same name — `DragCalc(MainVehicle)` and `DragCalc(SubChassis)` both
// use SharedTypes' DragCoeff — so a bare block name is not a usable answer for a
// blocks of the same name — `DragCalc(Sedan)` and `DragCalc(Truck)` both
// use a shared dictionary's DragCoeff — so a bare block name is not a usable answer for a
// variable that models share, which is every variable a dictionary exists to hold.
//
// `modelUri` rides along for the webview to GROUP by, and is not the same fact as
Expand Down Expand Up @@ -220,8 +220,8 @@ function toParamLink(origin: ParamOrigin): ParamLink {
* from core's SESSION — the models whose editor tab happened to be resolved in this
* window — in a shape that names the block and not the model. So whether a dictionary
* entry read `AFRConst, AFRCheck` or
* `AFR(EngineCtrl), AFRMonitor(EngineCtrl), MixTarget(EngineCtrl), AFRConst(FuelInjector),
* AFRCheck(FuelInjector)` depended on which models the user had opened — and merely
* `AFR(EngineLoop), AFRMonitor(EngineLoop), MixTarget(EngineLoop), AFRConst(FuelPath),
* AFRCheck(FuelPath)` depended on which models the user had opened — and merely
* CLICKING a Usage link registered the model it pointed at, degrading the very cell it
* was clicked from for as long as that model stayed registered. Two engines answering one
* column is the defect; this makes the workspace graph the one that does.
Expand Down Expand Up @@ -254,8 +254,8 @@ export function annotateVariableRows(
/**
* Drop the `(model)` qualifier from a usage inside the file the row itself belongs to.
*
* The qualifier is there because a variable can be SHARED: `DragCalc(MainVehicle)` and
* `DragCalc(SubChassis)` are two different blocks and the model is the only thing that
* The qualifier is there because a variable can be SHARED: `DragCalc(Sedan)` and
* `DragCalc(Truck)` are two different blocks and the model is the only thing that
* separates them. A MODEL-WORKSPACE variable is shared with nobody — every block that can
* read it is in the model whose workspace holds it — so in a model view the qualifier
* repeated the name of the open file on every link and disambiguated nothing.
Expand Down Expand Up @@ -291,11 +291,12 @@ function withoutOwnModel(link: BlockLink, sourceUri: string): BlockLink {
* `ModelBlockNode.toRow` publishes for exactly this purpose — and NEVER by the Name
* label. A label is unique only inside one system, so two `Gain` blocks in different
* subsystems would take each other's parameters; and a block Simulink recorded with no
* name at all (f14.slx's SID 65, whose `Name` attribute is a lone line break) has the
* synthetic label `<SID: 65>`, which matches no block in core's index and left the cell
* empty. Unlike a variable row, a block row IS emptied when the graph has nothing for
* it: the row came from this same model's parse, so the graph has seen the block, and
* "no parameters resolved" is an answer about it rather than a gap in what was read.
* name at all (SID 65 of a shipped Simulink demo model, whose `Name` attribute is a
* lone line break) has the synthetic label `<SID: 65>`, which matches no block in core's
* index and left the cell empty. Unlike a variable row, a block row IS emptied when the
* graph has nothing for it: the row came from this same model's parse, so the graph has
* seen the block, and "no parameters resolved" is an answer about it rather than a gap in
* what was read.
*/
export function annotateModelViewRows(
modelUri: string,
Expand Down
4 changes: 2 additions & 2 deletions src/webview/components/dex-tree-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,9 @@ interface ParamLinkCell {
}

// A Usage cell's block links, one group per model: `AFR, AFRMonitor,
// MixTarget(EngineCtrl); AFRConst, AFRCheck(FuelInjector)`. A dictionary variable is
// MixTarget(EngineLoop); AFRConst, AFRCheck(FuelPath)`. A dictionary variable is
// used by as many blocks as a model has, so naming the model once per block spent most
// of a narrow column repeating `(EngineCtrl)` and buried the block names the user is
// of a narrow column repeating `(EngineLoop)` and buried the block names the user is
// actually reading.
//
// Grouped on `modelUri`, NOT on the displayed `modelName`: the label is a stripped
Expand Down
2 changes: 1 addition & 1 deletion src/webview/table-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ function applyPendingNameSelection(): void {
// written before SIDs existed) is a NAME and matches the Name label; a block
// target is core's block KEY — the SID — which is not printed anywhere, so it is
// matched against the `_blockKey` the row publishes. Without that second pass a
// `blocks:65@f14.slx` click would open the model and select nothing, since the
// `blocks:65@<model>.slx` click would open the model and select nothing, since the
// row it means reads `<SID: 65>`. Name first, so every pre-SID target keeps its
// existing answer when a same-spelled key also exists.
//
Expand Down
4 changes: 2 additions & 2 deletions test/banners.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe('renderBanners', () => {
// OUTSIDE the strip — normal flow, ahead of a table pinned at inset:0 — so the
// table painted over it, measured in a real browser as a cleared spinner above
// the words "No data".
renderError(table, 'Failed to show allMidi.mat: Invalid string length', now);
renderError(table, 'Failed to show songs.mat: Invalid string length', now);
expect(el('dex-error').style.display).toBe('block');
expect(el('dex-error').textContent).toContain('Invalid string length');
expect(table.style.top).not.toBe('');
Expand All @@ -151,7 +151,7 @@ describe('renderBanners', () => {
// the offset from what is SHOWING rather than from its own argument. Before
// that, an `updateEntryRows` with nothing to say would have dropped the table
// back to the top of the panel and slid it under the error still above it.
renderError(table, 'Failed to show allMidi.mat: Invalid string length', now);
renderError(table, 'Failed to show songs.mat: Invalid string length', now);
renderBanners(table, {}, now);
expect(el('dex-error').style.display).toBe('block');
expect(table.style.top).not.toBe('');
Expand Down
12 changes: 6 additions & 6 deletions test/blockSidIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
// A block is identified by its SID, and its NAME is only a label — over real bytes, from
// the parse to the text in the cell.
//
// Found in ~/delite/f/f14.slx, in two shapes at once. A block name is unique only inside
// its own system, and that model has four blocks named `Gain` in four different
// subsystems; keyed by name they collapsed into ONE row whose Usage read
// Found in a shipped Simulink demo model, in two shapes at once. A block name is unique
// only inside its own system, and that model has four blocks named `Gain` in four
// different subsystems; keyed by name they collapsed into ONE row whose Usage read
// `Gain=Mq, Gain=Zw, Gain=Kf, Gain=Zw` — four blocks' parameters in one cell — and all
// four shared the row id `f14.slx/blocks/Gain`, so the Property Inspector, a selection,
// four shared the row id `<model>.slx/blocks/Gain`, so the Property Inspector, a selection,
// and a Usage link could not tell them apart. And the model holds a Constant whose `Name`
// attribute Simulink wrote as a lone line break (`Name="&#xA;"` SID 65): it normalizes to
// the empty string, so the row showed a BLANK Name, took the id `f14.slx/blocks/`, and its
// the empty string, so the row showed a BLANK Name, took the id `<model>.slx/blocks/`, and its
// Usage link — whose text is the block name — was an empty anchor with nothing to click.
//
// Core v1.8.0 splits the two facts (`blockKey` = the SID, `blockLabel` = the name or
Expand Down Expand Up @@ -90,7 +90,7 @@ describe('the workspace graph over same-named and nameless blocks', () => {
});

it('gives the nameless block a printable label and a followable link', () => {
// The f14 block the user asked about, over the same `Uo` it reads there. The cell text
// That model's block the user asked about, over the same `Uo` it reads there. The cell text
// was empty — an anchor with no characters in it cannot be clicked — and the target
// named no block at all.
expect(graph().blocksUsing(DICT, 'Uo')).toEqual([
Expand Down
Loading
Loading