Skip to content
Closed
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
95 changes: 95 additions & 0 deletions .github/workflows/noglint-profile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: no-glint CPU profile (experiment)

# Profiles the benchmark's `no glint` case on the runner, control (main) and
# experiment (this branch) side by side, and uploads the .cpuprofile files.
# Open the artifact's profiles in Chrome DevTools > Performance > Load profile.

on:
pull_request:
workflow_dispatch:

jobs:
profile:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: pnpm

- name: Build experiment (this branch)
run: |
pnpm install --frozen-lockfile
pnpm build

- name: Build control (main)
run: |
git worktree add "$RUNNER_TEMP/control" origin/main
cd "$RUNNER_TEMP/control"
pnpm install --frozen-lockfile
pnpm build

- name: Profile the `no glint` bench case, 3 runs per side
env:
# Same environment as test/validate.bench.mjs gives the case.
HVE_NO_CACHE: '1'
HVE_TS_BACKEND: tsgo
run: |
files=(test/bench/large.gts examples/*.gts)
echo "files: ${#files[@]}"
{
echo "| side | run | wall ms |"
echo "|---|---|---|"
for side in control experiment; do
dist="$GITHUB_WORKSPACE"
[ "$side" = control ] && dist="$RUNNER_TEMP/control"
mkdir -p "$RUNNER_TEMP/prof/$side"
for i in 1 2 3; do
start=$(date +%s%N)
node --cpu-prof --cpu-prof-dir="$RUNNER_TEMP/prof/$side" --cpu-prof-name="run$i.cpuprofile" \
"$dist/dist/run.js" --no-glint "${files[@]}" > /dev/null 2> "$RUNNER_TEMP/prof/$side/run$i.stderr" || true
end=$(date +%s%N)
echo "| $side | $i | $(( (end - start) / 1000000 )) |"
done
done
} | tee -a "$GITHUB_STEP_SUMMARY"

- name: Summarise hottest functions per side
run: |
node - <<'JS' | tee -a "$GITHUB_STEP_SUMMARY"
const fs = require('node:fs');
const path = require('node:path');
const root = path.join(process.env.RUNNER_TEMP, 'prof');
for (const side of ['control', 'experiment']) {
const self = new Map();
let total = 0;
for (const file of fs.readdirSync(path.join(root, side)).filter((f) => f.endsWith('.cpuprofile'))) {
const prof = JSON.parse(fs.readFileSync(path.join(root, side, file), 'utf8'));
const byId = new Map(prof.nodes.map((n) => [n.id, n]));
const deltas = prof.timeDeltas;
prof.samples.forEach((id, i) => {
const n = byId.get(id);
const cf = n.callFrame;
const key = `${cf.functionName || '(anonymous)'} ${cf.url.replace(/^.*\/(dist|node_modules)\//, '$1/')}:${cf.lineNumber + 1}`;
const dt = deltas[i] ?? 0;
self.set(key, (self.get(key) ?? 0) + dt);
total += dt;
});
}
console.log(`\n### ${side} — top 25 self time (3 runs, ${(total / 1e6).toFixed(0)} ms sampled)\n`);
console.log('| self ms | function |'); console.log('|---|---|');
for (const [key, us] of [...self].sort((a, b) => b[1] - a[1]).slice(0, 25)) console.log(`| ${(us / 1e3).toFixed(0)} | \`${key}\` |`);
}
JS

- uses: actions/upload-artifact@v4
with:
name: noglint-cpuprofiles
path: ${{ runner.temp }}/prof
12 changes: 7 additions & 5 deletions lib/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,19 @@ function declaresContentMappers(tsconfigPath: string): boolean {
}

/**
* Which backend `backendFor` would pick, as a cache-key component, without
* loading TypeScript: the forced kind, or tsgo (with its package and
* version) when the tsconfig declares `contentMappers` and a TypeScript 7
* package resolves.
* Which backend `backendFor` would pick, as a cache-key component: the
* forced kind, or tsgo (with its package and version) when the tsconfig
* declares `contentMappers` and a TypeScript 7 package resolves. Whether
* that package then loads depends on the Node version (`require()` of its
* ESM API needs 22.12+), so Node is part of the component too; the
* package is not loaded here — a cached replay never needs it.
*/
export function backendKindFor(tsconfigPath: string): string {
const forced = process.env['HVE_TS_BACKEND'];
if (forced === 'ts6') return 'ts6';
if (forced === 'tsgo' || declaresContentMappers(tsconfigPath)) {
const pkg = resolveTsgoPackage(path.dirname(tsconfigPath));
if (pkg) return `tsgo:${pkg.name}@${pkg.version}`;
if (pkg) return `tsgo:${pkg.name}@${pkg.version}:node${process.versions.node}`;
if (forced === 'tsgo') return 'none';
}
return 'ts6';
Expand Down
26 changes: 7 additions & 19 deletions lib/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';

import type { ComponentAttrs } from './builtin-components.js';
import { dependencySha, sha256 } from './deps.js';
import { dependencySha, sha256, tsconfigChainSha } from './deps.js';

// Walk up from this module looking for the nearest `package.json` so the
// version is found regardless of whether we're running from source
Expand Down Expand Up @@ -139,22 +139,10 @@ interface CacheEntry {
componentAttrMap: Array<[string, ComponentAttrs]>;
}

// In-memory cache for the SHA of each tsconfig file (read once per
// process; tsconfigs rarely change mid-run).
const tsconfigShaCache = new Map<string, string>();

// The tsconfig and every config it extends; `lib/deps.ts` re-validates
// the chain against the file system.
function getTsconfigSha(tsconfigPath: string): string {
const cached = tsconfigShaCache.get(tsconfigPath);
if (cached !== undefined) return cached;
let sha: string;
try {
const contents = fs.readFileSync(tsconfigPath, 'utf8');
sha = sha256(contents);
} catch {
sha = 'no-tsconfig';
}
tsconfigShaCache.set(tsconfigPath, sha);
return sha;
return tsconfigChainSha(tsconfigPath);
}

// Walk up from a file to find the project root (where node_modules/
Expand Down Expand Up @@ -306,10 +294,10 @@ interface TransformCacheEntry {
}

/** Everything the transform's output depends on besides the plugin itself. */
// Without Glint nothing crosses file boundaries, so the closure is not
// part of the key (and not computed).
// The closure matters with Glint off too: the resolver reads an imported
// component's template to substitute its tag.
function dependenciesForKey(filename: string, contents: string, tsconfigPath: string | null): string {
return process.env['HVE_GLINT'] === '0' ? 'no-glint' : dependencySha(filename, contents, tsconfigPath);
return CACHE_DISABLED ? 'disabled' : dependencySha(filename, contents, tsconfigPath);
}

export function transformCacheKey(filename: string, data: string, tsconfigPath: string | null, backendKind: string): string {
Expand Down
Loading
Loading