Repository navigation
Read a grid cell's digits in linear time, and an MSP label's kind off an own key - #53
Merged
Merged
Conversation
…hip in linear time The Variable Editor grid tests every cell against NUMERIC to decide the block's alignment, and it read a decimal as `\d+\.?\d*`. With no dot that is `\d+\d*`, and a backtracking engine tries every split of a run of digits between the two before it gives up, so a cell that starts with a long run of digits and is not a number cost the square of its length: 1,000 digits and an `x` took 2.2 ms in the bare pattern, 10,000 took 215 ms and 100,000 took 21.4 s. A file reaches it. A binary dictionary's complex value is held as its text, core's displayElements() hands each element over as that text, less MATLAB's `.0`, and no complex element is a number, so the first cell is the one tested. sparse_binary.sldd with dComplex's first element lengthened to 50,000 digits and `.0+2.0i` held the grid's open for 7.7 s, and for 7.5 s with the digits and an `i`. `\d+(?:\.\d*)?` matches the same strings and leaves every group where it was, as core's rewrite of the same fragment did (mathworks/data-explorer-core#74). The bare pattern takes 0.4 ms on 100,000 digits and 4.4 ms on a million, and the grid test below runs in 40 to 50 ms, the dictionary's parse included. The old and new patterns agree on test() and on every exec() group for all 2,593,742,460 strings of up to nine characters over `019.eE+-ix` and the space, all 286,331,153 of up to seven over those and `InfNa`, and all 579,195 sequences of up to five tokens from those and `Inf`, `NaN`, `Infinity`, `12` and `1.5`. Both patterns are read out of the two files rather than retyped. The harness tells a wrong rewrite apart: of the 235,794,769 strings of up to eight characters, requiring a digit after the dot differs on 27,981, dropping the dot on 89,892, and making the new group capturing on the exec() groups of all 219,969 that match. A sweep of the other 28 regexes in src/ found one more of the kind. removeToken in rowFilter.ts trimmed the text before a removed chip with `.replace(/\s+$/, '')`, which is anchored at the end only, so a run of whitespace that is not at the end cost the square of its length: 160,000 spaces took 15.7 s in the bare pattern, and 100,000 of them anywhere before a chip held its removal for 8.4 to 9.4 s. The text is typed: the filter bar, or a header popup's value, which formatToken quotes and keeps whole. A chip's x, Backspace into the chips and a popup's Clear all come here. trimEnd() removes the same characters, `\s` and trim both being WhiteSpace and LineTerminator: the two agree on every code point, alone and in four contexts, and on all 111,111,111 strings of up to eight characters over ten symbols mixing ASCII and Unicode whitespace with other characters, where trim(), a spaces-only pattern and an ASCII-only one each differ. The removal takes about a millisecond now. Of the remaining 27, the nine with a quantifier over their input stay linear when pumped to 160,000 characters, and the rest match a single character, a literal, or at one anchored position. gridNumericScan.test.ts opens the grid on the dictionary rewritten that way, through getModelFromBytes and matrixPayload, and pins which cells read as numbers. removeTokenLinear.test.ts removes a chip after a long run of whitespace, plain and quoted. The four timing tests fail against the old code, at 7.5 to 9.4 s against a 1 s bound, and pass with the new; a grid pattern that requires a digit after the dot, or has no dot, fails the alignment table.
… neither half detectMsp looked a project label's name up in LABEL_KIND, a plain object, and the name is the project store's own text. On a plain object toString, toLocaleString, valueOf, constructor, hasOwnProperty, isPrototypeOf, propertyIsEnumerable, __proto__ and the four __define/__lookup accessors answer from Object.prototype, each of them truthy, so an MSP label of any of those names was recorded against its id as one of the two halves. On a well-formed store that went unseen: the member carrying it was pushed as a dictionary of that "kind", and the interface-first ordering dropped it. A label sharing a Location with InterfaceDictionary or PrivateDictionary overwrote that half's entry instead, and the half went missing from the page; a project that lost its private half that way read as a shared interface rather than a component. MATLAB does not write two labels at one Location, but a monolithic store is one XML document, and core's parseProject reads both labels at the one id. The lookup is now of an own key, through Object.hasOwn, as mathworks/data-explorer-core#73 made classificationOf's. mspLabelOwnKey.test.ts parses monolithic stores through core's parseProject, from the package barrel: each of the twelve names as an MSP label with an id of its own and a member carrying it, and sharing a Location with each half. 9 of the 36 failed before, each with the half missing: every name that sorts after the label it shares a Location with, since core sorts the catalog by name and the later label overwrote the earlier. All 36 pass now, and msp.test.ts's 19 are unchanged. The other two lookups named with this one are not reached by a file. dex-icon's ALIASES is indexed by a row's iconId, and iconMap's MAP by a search result's kind: every icon id is a constant of core's or of this extension's. Given `toString`, dex-icon would request icons/function toString() { [native code] }.svg. A census of computed-key reads and writes in src/, through the type checker, finds no other table or accumulator that a file's or a user's text can index with an inherited name.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two fixes of the kinds data-explorer-core fixed in v1.36.4, in this repo's own code: a backtracking regex a file's text reaches (mathworks/data-explorer-core#74), and a plain-object lookup a file's text can index with an inherited name (mathworks/data-explorer-core#73). One commit each. No version bump, no CHANGELOG entry, no package.json or lockfile change.
1. The Variable Editor grid's number test, and one more quadratic trim
The grid (
src/webview/components/dex-matrix-grid.ts)The grid tests every cell against
NUMERICto decide whether the whole block is right-aligned. It read a decimal as\d+\.?\d*. With no dot that is\d+\d*, and a backtracking engine tries every split of a run of digits between the two before it gives up. A cell that starts with a long run of digits and is not a number therefore costs the square of its length.How a file reaches it. A binary dictionary holds a complex value as text, the way MATLAB writes it:
<P Name="Value" Class="double" IsComplex="1" Dimension="3*4">1.0+2.0i 0.0+0.0i ...</P>. Core'sparseCdatakeeps each element as that text with MATLAB's.0removed.displayElements()passes it on unchanged,matrixPayloadplaces it, and the grid tests it when it opens. A complex element is never a number, so the first cell in canonical order is the one the grid tests. I checked this route end to end: the test below makescells[0]the file's own digits. A text dictionary's plain-text{_type: 'cdata'}value takes the sameparseCdatapath; I probed that through core'sDataModel.addDataSourceonly, and it has no test here. Whether a typed cell also reaches the grid with such text was not measured here.Before / after (this machine, Node 24.14.1):
xxxxThe same quadratic growth showed with an
i,+1ioretail in place of thex(21.0 to 21.4 s at 100,000 digits).Through the real route,
sparse_binary.sldd(MATLAB-written, fromtest-integration/fixtures/sparse) had dComplex's first element replaced with 50,000 digits followed by.0+2.0i. With the old pattern, opening the grid on it took 7,674 ms, and 7,540 ms with the digits followed byi. With the new pattern, each whole test, including the dictionary parse, takes 22 to 50 ms.The rewrite is
\d+(?:\.\d*)?, the same rewrite core made. The group is non-capturing, so the capture-group numbering does not change.Equivalence, old and new, compared on
test()and on everyexec()group (withindex). Both patterns are read from the two files (git show origin/main:and the worktree), not retyped:test()mismatchesexec()mismatches0 1 9 . e E + - i xand spaceI n f N a1 9 . e + - i x, space,Inf NaN Infinity 12 1.5The same harness, with a deliberately wrong rewrite swapped in, over the 235,794,769 strings of ≤ 8 chars:
\d+(?:\.\d+)?test()andexec())\d+test()andexec())\d+(\.\d*)?test(), 219,969 onexec()(every matching string)removeToken (
src/webview/rowFilter.ts)I checked the other 28 regexes in
src/for the same failure and found one more.removeTokencloses the gap left by a removed filter chip, and it trimmed the text before the chip with.replace(/\s+$/, ''). That pattern is anchored at the end only. From each character of a whitespace run that is not at the end, the engine takes the rest of the run, fails on the next character, and gives the run back one character at a time.How it is reached: a chip's ×, Backspace into the chips, or a header popup's Clear. The text is the user's own: typed or pasted into the filter bar, or a popup value, which
formatTokenquotes and keeps whole, inner spaces included. I found no file-derived route.Before / after:
'a' + 160,000 spaces + 'b ': 15,672 ms.The rewrite is
trimEnd(). The spec defines both\sand the trim set as WhiteSpace plus LineTerminator. Measured agreement:\t,\n, U+00A0, U+3000, U+FEFF, U+200B (which is not\s),a,"and=.trim()differs on 25 code-point cases,/ +$/on 96, and an ASCII-only class on 76.The start of the gap still uses
/^\s+/. It is tried at one position only, which is linear.The regex census
Line numbers in this section are origin/main's (555de7b).
src/holds 102.tsfiles with 29 regexes: 28 literals plus onenew RegExp, collected from the TypeScript AST. No string-argument.match/.searchexists, and the.htmlfiles contain no inline script.NUMERIC/\s+$//^\s+//(?:[^\s"]+|"[^"]*")+/g+, so it never backtracks into the splitssplit(/\s+/)/\s+/greplace/^[ \t]*//^(\t+| +)\S/m/^[0-9]+$//\n$//\.xml$/i/^(>=|<=|!=|~=|=|>|<)/No other
\d+\.?\d*-style fragment appears anywhere in the tracked tree.2. Plain-object lookups keyed by a file's text
The three sites named
Line numbers in this section are origin/main's (555de7b).
src/host/mspProject.ts:83,LABEL_KIND[label.name]: reached by a file, and fixed.parseProjectreads a label's name from its<Info Name>(XML layouts) or its TOML key.toString,toLocaleString,valueOf,constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,__proto__and the four__define/__lookupaccessors each answer fromObject.prototype, each truthy. So an MSP label of such a name was recorded against its id as one of the two halves.order()dropped it.InterfaceDictionaryorPrivateDictionary: it overwrote that half's entry, and the half disappeared from the page. A project that lost its private half this way read as a shared interface (role: 'common') rather than a component; I checked that against the old code.Project.xmlis a single document, though, and core reads both labels at one id; the test proves that precondition.Object.hasOwn(LABEL_KIND, label.name) ? LABEL_KIND[label.name] : undefined, matching how #73 made core'sclassificationOfread own keys.src/webview/components/dex-icon.ts:29,ALIASES[iconId]: not reached by a file, left alone.iconIdcomes from: a row'sName.iconId(core'snode.icon, all constants), the add gallery's tiles (constants insrc/common/addCatalog.ts),'wsTable', and the drag tooltip, which copies the row's.toStringwould do: measured in happy-dom, the component requestsicons/function toString() { [native code] }.svg;constructorgivesfunction Object() { [native code] }.svgand__proto__gives[object Object].svg. That is a broken image, with no throw.src/host/iconMap.ts:24,MAP[id]: not reached by a file, left alone.idcomes from:themeIconFor's only caller passesICON_ID_BY_KIND[rec.kind], a closedEntryKindunion.svgIconFordoes no lookup.toStringwould do: a prediction, not run (the module importsvscode): aThemeIconwhoseidis a function.The census
A TypeScript-checker pass over
src/found 41 computed-key reads and writes (obj[key],key in obj) on a non-array object with a string-typed key. 13 have a closed key type: a literal union or an enum-like constant. The other 28:out[childRel] = …resources/project/…/<name>/, so it can never be an inherited nameobj[propDef.name]toPIObjectwrites an own key for every property it lists: schema keys, orOther.-prefixedbag[key]'uuid','namespace','isderived'parts[member] = data__proto__part.member, own keysObject.keys/[&<>"]/state[id]labels[key]Maps (labelMap,SUBSTRING_FILTER_COLUMNS); only a resolved column key indexes the objectDEFAULT_WIDTHS,columnLabels,columnGroups,row[col])COLUMNS/COLUMN_LABELSconstants of the hostAll ten
inexpressions insrc/use literal keys ('links','paramLinks','blockLinks').Tests
test/gridNumericScan.test.ts(happy-dom, 41 tests). It rewritessparse_binary.slddas described above, loads it throughgetModelFromBytesandmatrixPayload, and opens the grid against a 1 s bound. A control checks that long digit strings which are numbers stay right-aligned, and a table of 38 cells pins which cells read as numbers.test/removeTokenLinear.test.ts(3 tests). It removes a chip after 100,000 spaces, once bare and once inside a quoted value, against a 1 s bound, and checks the gap-closing over 16 whitespace characters.test/mspLabelOwnKey.test.ts(36 tests). It parses monolithic stores through core'sparseProjectfrom the package barrel. Each of the twelve inherited names appears as an attached MSP label with its own id, and also sharing a Location with each half.msp.test.ts's 19 are unchanged.No existing test was modified.
Verification
npm run verifyon the final tree: exit 0. typecheck, build, build:web, 167 test files / 3,208 tests passed, check:leak's three checks OK.addEntryWiring.test.ts'sbeforeAllexceeding vitest's 10 s hook timeout, at a load average of about 18 on 12 cores. That file passed alone in 7.5 s, and the full rerun was green (166 files / 3,172 tests).npm run test:integration: exit 0, 168 passing, 1 pending, on VS Code 1.141.0, which the fresh worktree downloaded.Notes
__proto__.unzipSynccollects members into a plain object, so that member becomes the result's prototype instead of a key; I measured this on a hand-renamed zip.BinarySlddEditorProvider's pass-through bag therefore never sees it, and a save would drop that member. MATLAB does not write one. The loss is in the dependency, not in this repo's code, and is left for a separate decision.MatlabVariableNode'sMCOS_ICON_MAP[this._opaqueClassName]is indexed by a class name read from a.matfile. As a prediction, not run: an object of classtoStringwould get an icon that is a function, and JSONpostMessagewould drop it before it reaches the webview. That is for core.matrixPayload.ts:115's/^[0-9]+$/is the tenth; it is listed and timed above. The commit was not amended.