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
9 changes: 5 additions & 4 deletions blank.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { preprocess, traverse } from '@glimmer/syntax';
import { traverse } from '@glimmer/syntax';
import { parseTemplate } from './lib/parse-template.js';
import type { AST } from '@glimmer/syntax';

// At runtime, AST node `loc` fields are `SourceSpan` instances that
Expand Down Expand Up @@ -1532,7 +1533,7 @@ function blankTemplateContent(
// (e.g. long-form `{{!-- ... --}}` vs short-form `{{! ... }}` comments;
// exact whitespace) — same flag `ember-estree` uses for its
// `templateOnly: true` path.
ast = preprocess(content, { mode: 'codemod' });
ast = parseTemplate(content);
} catch (err) {
return { content, error: err instanceof Error ? err : new Error(String(err)) };
}
Expand Down Expand Up @@ -2724,7 +2725,7 @@ function blankTemplateContentMultipass(
): Array<BlankResult | BlankErrorResult> {
// Cap=0 disables multipass — the tree would be empty anyway, and
// every fallback path below ends in a single `blankTemplateContent`
// call. Short-circuit before the local `preprocess()` so the
// call. Short-circuit before the local `parseTemplate()` so the
// disable path parses once instead of twice.
const cap = readMaxConditionalBranches();
if (cap === 0) {
Expand All @@ -2734,7 +2735,7 @@ function blankTemplateContentMultipass(
}
let ast: AST.Template;
try {
ast = preprocess(content, { mode: 'codemod' });
ast = parseTemplate(content);
} catch (err) {
return [{ content, error: err instanceof Error ? err : new Error(String(err)) }];
}
Expand Down
19 changes: 19 additions & 0 deletions lib/parse-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { preprocess, type AST } from '@glimmer/syntax';

// A template is parsed by the transformer, by the multipass branch walk
// and once per pass; the resolver parses the same component templates for
// every consumer. One parse per content string, oldest entry evicted. Parse
// errors are not cached: the callers report them.
const MAX_ENTRIES = 64;
const cache = new Map<string, AST.Template>();

export function parseTemplate(content: string): AST.Template {
const cached = cache.get(content);
if (cached) return cached;
const ast = preprocess(content, { mode: 'codemod' });
cache.set(content, ast);
if (cache.size > MAX_ENTRIES) {
cache.delete(cache.keys().next().value as string);
}
return ast;
}
7 changes: 2 additions & 5 deletions lib/resolver/walk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,15 @@
// choice — the resolver picks the right tag (preferring the yield-ancestor
// when content-permission validation hinges on it).

import { preprocess, type AST } from '@glimmer/syntax';
import type { AST } from '@glimmer/syntax';
import { Preprocessor } from 'content-tag';
import type * as TS from 'typescript';
import type { TsSyntax } from '../backend/types.js';
import path from 'node:path';
import fs from 'node:fs';

import { STRUCTURAL_CHILD_TAGS } from '../element-sets.js';

function parseTemplate(content: string): AST.Template {
return preprocess(content, { mode: 'codemod' });
}
import { parseTemplate } from '../parse-template.js';

const ctPreprocessor = new Preprocessor();

Expand Down
175 changes: 175 additions & 0 deletions test/bench/large.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import Component from '@glimmer/component';
import { on } from '@ember/modifier';
import { concat } from '@ember/helper';

// Generated: a large template with nested conditionals and helper calls,
// with a dozen conditionals so multipass enumeration is exercised. Used
// by the benchmarks.
export default class Large extends Component<{ Blocks: { default: [] } }> {
compact = false;
title = 'Row';
placeholder = 'nothing';
state = 'idle';
items = [{ active: true, href: '#', label: 'one' }];
show0 = true; show1 = false; show2 = true; show3 = false; show4 = true; show5 = false; show6 = true;
select = () => {};

<template>
{{#if this.show0}}
<section class={{if this.compact 'compact' 'wide'}} data-row='0'>
<h3 title={{concat 'row ' 0}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
{{#if this.show1}}
<section class={{if this.compact 'compact' 'wide'}} data-row='1'>
<h3 title={{concat 'row ' 1}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
{{#if this.show2}}
<section class={{if this.compact 'compact' 'wide'}} data-row='2'>
<h3 title={{concat 'row ' 2}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
{{#if this.show3}}
<section class={{if this.compact 'compact' 'wide'}} data-row='3'>
<h3 title={{concat 'row ' 3}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
{{#if this.show4}}
<section class={{if this.compact 'compact' 'wide'}} data-row='4'>
<h3 title={{concat 'row ' 4}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
{{#if this.show5}}
<section class={{if this.compact 'compact' 'wide'}} data-row='5'>
<h3 title={{concat 'row ' 5}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
{{#if this.show6}}
<section class={{if this.compact 'compact' 'wide'}} data-row='6'>
<h3 title={{concat 'row ' 6}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
{{#if this.show0}}
<section class={{if this.compact 'compact' 'wide'}} data-row='7'>
<h3 title={{concat 'row ' 7}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
{{#if this.show1}}
<section class={{if this.compact 'compact' 'wide'}} data-row='8'>
<h3 title={{concat 'row ' 8}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
{{#if this.show2}}
<section class={{if this.compact 'compact' 'wide'}} data-row='9'>
<h3 title={{concat 'row ' 9}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
{{#if this.show3}}
<section class={{if this.compact 'compact' 'wide'}} data-row='10'>
<h3 title={{concat 'row ' 10}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
{{#if this.show4}}
<section class={{if this.compact 'compact' 'wide'}} data-row='11'>
<h3 title={{concat 'row ' 11}}>{{this.title}}</h3>
<ul>
{{#each this.items as |item|}}
<li class={{if item.active 'active' 'idle'}}><a href={{item.href}}>{{item.label}}</a></li>
{{/each}}
</ul>
<button type='button' disabled={{eq this.state 'busy'}} {{on 'click' this.select}}>{{yield}}</button>
</section>
{{else}}
<p class='muted'>{{this.placeholder}}</p>
{{/if}}
</template>
}
17 changes: 11 additions & 6 deletions test/validate.bench.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const FIXTURES = {
'small template': fixture('examples/h32-yield-and-ambiguous-submit.gts'),
'medium template': fixture('examples/heuristic-masks-real-bug.gts'),
'cross-file resolution': fixture('test/glint-fixtures/curry-multi-level-consumer.gts'),
'large template': fixture('test/bench/large.gts'),
};

// Warm up: opens the project and compiles the hot paths before anything
Expand All @@ -75,12 +76,16 @@ function validate(cliArgs, env = {}) {

// A fixed subset keeps each run short; the costs these cases guard against
// (backend start-up, uncached per-file work) show at any size.
const SUBSET = readdirSync(resolve(ROOT, 'examples'))
.filter((f) => f.endsWith('.gts'))
.sort()
.slice(0, 20)
.map((f) => `examples/${f}`);
const ONE = [SUBSET[0]];
const SUBSET = [
'test/bench/large.gts',
...readdirSync(resolve(ROOT, 'examples'))
.filter((f) => f.endsWith('.gts'))
.sort()
.slice(0, 20)
.map((f) => `examples/${f}`),
];
// A small file: this case measures start-up, not template work.
const ONE = [SUBSET[1]];
const CACHED = { HVE_NO_CACHE: '' };
const PROCESS_CASES = {
'cold run (cache off)': () => validate(['--glint', ...SUBSET]),
Expand Down
6 changes: 3 additions & 3 deletions transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
} from 'html-validate';
import { createRequire } from 'node:module';

import { preprocess } from '@glimmer/syntax';
import { parseTemplate } from './lib/parse-template.js';

import {
blankTemplateContent,
Expand Down Expand Up @@ -258,7 +258,7 @@ function* transformGlimmer(source: Source): Generator<Source, void, unknown> {
let classicTagMap: Map<string, string> | null = null;
let classicAttrMap: Parameters<typeof blankTemplateContent>[4] | null = null;
try {
const ast = preprocess(data, { mode: 'codemod' });
const ast = parseTemplate(data);
const maps = buildResolutionMaps(filename, ast);
classicTagMap = maps.componentTagMap;
classicAttrMap = maps.componentAttrMap;
Expand Down Expand Up @@ -398,7 +398,7 @@ function* transformGlimmer(source: Source): Generator<Source, void, unknown> {
let attrMap = glintComponentAttrMap;
if (!tagMap) {
try {
const ast = preprocess(tpl.contents, { mode: 'codemod' });
const ast = parseTemplate(tpl.contents);
const maps = buildResolutionMaps(filename, ast);
tagMap = maps.componentTagMap;
attrMap = maps.componentAttrMap;
Expand Down
Loading