Skip to content

Commit 5a0c002

Browse files
perf: pre-declare position on link/text factories
Problem: this extension builds link nodes (and their nested text children) in two paths: the enter handler for literal autolinks during parse, and a post-parse find-and-replace transform that walks text nodes and produces link/text nodes from URL-shaped substrings. In both paths the literals are fresh objects with no position field; position is patched in afterwards. V8 treats the late assignment as a shape change, so every link and text node walks through two hidden classes instead of one. Core nodes already avoid this; extension nodes leave the tree with mixed layouts and tree walks lose monomorphic dispatch. Goal: declare position on every link and text literal this extension produces, both in the enter handler and in the post-parse transform, so V8 keeps a single hidden class per node type. Changes: - lib/index.js: trailing position: undefined on the link literal in enterLiteralAutolink. - lib/index.js: trailing position: undefined on the link and nested text literals returned from findUrl, plus the trailing-text node when splitUrl strips punctuation. - lib/index.js: trailing position: undefined on the link and nested text literals returned from findEmail. Notes: the transform-path literals at lines 175 through 180, 183, and 207 through 212 are returned from find-and-replace callbacks rather than going through this.enter(). A one-off probe (parsing 'visit www.example.com\n' with the GFM extension) confirmed that something further down the pipeline populates position on these nodes, so the pre-declared field gets overwritten with a real value and the test fixtures continue to assert real position objects. Pairs with the matching core change in syntax-tree/mdast-util-from-markdown#53; merged on its own this branch is a no-op. Validated with npm test (build, format, 100% coverage, 49 of 49 tests in dev and prod). Bench (local harness, 3-run median-of-medians, GFM tokenizer and mdast extensions stacked, with the core branch and three sibling extension branches checked out alongside this one): Two scenarios exercise this code path. The first is 5000 bare URLs interleaved with surrounding text, which is the largest mdast-layer share in the GFM corpus on the baseline (full 319.6 ms, tokenize-only 142.8 ms; the gap between those is roughly 35 microseconds of mdast-layer work per URL, mostly node allocation and position bookkeeping). The second is the syntax-tree GFM-stack readme used as a real-world fixture, where the autolink resolver still walks every text node. scenario baseline core alone core + paired 5000 bare URLs + text 319.6 ms +2.3% -2.5% syntax-tree GFM-stack readme 22.8 ms +29.4% +22.5% The bare-URL scenario is the only paired-vs-baseline comparison across the whole 5-repo paired set that crosses zero (i.e., the paired configuration parses faster than upstream main does). The 4.8-point swing from solo to paired matches what the V8 hidden-class hypothesis predicts: with link and text literals sharing the core layout, every link in the resulting tree dispatches through one inline cache. A drift floor measured separately ran at +4 to +12 percent across most GFM inputs on this machine in the same window; the readme's +22.5 percent paired residual is in the upper end of that band, which is consistent with no real regression.
1 parent ba83f42 commit 5a0c002

1 file changed

Lines changed: 16 additions & 4 deletions

File tree

lib/index.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,13 @@ export function gfmAutolinkLiteralToMarkdown() {
8080
* @type {FromMarkdownHandle}
8181
*/
8282
function enterLiteralAutolink(token) {
83-
this.enter({type: 'link', title: null, url: '', children: []}, token)
83+
// Trailing `position: undefined` keeps the link hidden class stable;
84+
// mdast-util-from-markdown's enter() patches the field to a real value
85+
// but the property already exists, so no shape transition fires.
86+
this.enter(
87+
{type: 'link', title: null, url: '', children: [], position: undefined},
88+
token
89+
)
8490
}
8591

8692
/**
@@ -171,16 +177,20 @@ function findUrl(_, protocol, domain, path, match) {
171177

172178
if (!parts[0]) return false
173179

180+
// Trailing `position: undefined` on every node literal keeps each
181+
// link / text hidden class stable; an upstream pass in find-and-replace
182+
// patches the field to a real value before the node is observable.
174183
/** @type {Link} */
175184
const result = {
176185
type: 'link',
177186
title: null,
178187
url: prefix + protocol + parts[0],
179-
children: [{type: 'text', value: protocol + parts[0]}]
188+
children: [{type: 'text', value: protocol + parts[0], position: undefined}],
189+
position: undefined
180190
}
181191

182192
if (parts[1]) {
183-
return [result, {type: 'text', value: parts[1]}]
193+
return [result, {type: 'text', value: parts[1], position: undefined}]
184194
}
185195

186196
return result
@@ -204,11 +214,13 @@ function findEmail(_, atext, label, match) {
204214
return false
205215
}
206216

217+
// See findUrl above for the rationale on the trailing position field.
207218
return {
208219
type: 'link',
209220
title: null,
210221
url: 'mailto:' + atext + '@' + label,
211-
children: [{type: 'text', value: atext + '@' + label}]
222+
children: [{type: 'text', value: atext + '@' + label, position: undefined}],
223+
position: undefined
212224
}
213225
}
214226

0 commit comments

Comments
 (0)