Skip to content

feat: add Chrome tracing for performance measurement#663

Draft
branchseer wants to merge 38 commits intomainfrom
feat/add-tracing-instrumentation
Draft

feat: add Chrome tracing for performance measurement#663
branchseer wants to merge 38 commits intomainfrom
feat/add-tracing-instrumentation

Conversation

@branchseer
Copy link
Member

Summary

  • Add Chrome DevTools timeline tracing support via VITE_LOG=debug VITE_LOG_OUTPUT=chrome-json
  • Update vite-task dependency (feat: add tracing instrumentation for performance measurement vite-task#178) with tracing instrumentation across all key code paths
  • Enable tracing in e2e-test workflow and upload trace artifacts for analysis
  • Run e2e commands twice to measure vite-task caching performance

Changes

vite_shared tracing

  • Rewrite init_tracing() to support three output modes: chrome-json, readable, stdout
  • Add VITE_LOG_OUTPUT environment variable
  • Add tracing and tracing-chrome dependencies
  • Return a guard from init_tracing() to ensure Chrome trace files are flushed

e2e-test.yml

  • Enable VITE_LOG=debug and VITE_LOG_OUTPUT=chrome-json in e2e test steps
  • Run commands twice (first run + cache run) to measure caching performance
  • Collect and upload trace files as artifacts

vite-task dependency

Test plan

  • CI passes
  • E2E tests pass with tracing enabled
  • Trace artifacts are uploaded successfully
  • Trace files can be loaded in Chrome DevTools (chrome://tracing)

🤖 Generated with Claude Code

@branchseer branchseer added the test: e2e Auto run e2e tests label Mar 1, 2026
@branchseer branchseer marked this pull request as draft March 2, 2026 03:01
branchseer and others added 9 commits March 4, 2026 01:04
…n` config

Add a new `vite_static_config` crate that uses oxc_parser to statically
extract JSON-serializable fields from vite.config.* files without needing
a Node.js runtime. The `VitePlusConfigLoader` now tries static extraction
first for the `run` config and falls back to NAPI only when needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The config file resolution order was .ts first, but Vite resolves
.js, .mjs, .ts, .cjs, .mts, .cts — matching that order now.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…olver

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
resolve_static_config now returns Option<FxHashMap<..., StaticFieldValue>>:
- None: config is not analyzable (no file, parse error, no export default,
  or exported value is not an object literal) — caller should fall back to NAPI
- Some(map): config was successfully analyzed
  - Json(value): field extracted as pure JSON
  - NonStatic: field exists but value is not a JSON literal
  - key absent: field does not exist in the config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tching

- PropertyKey::static_name() replaces property_key_to_string and property_key_to_json_key
- TemplateLiteral::single_quasi() replaces manual quasis/expressions check
- Expression::is_specific_id() replaces is_define_config_call helper
- ArrayExpressionElement::is_elision()/is_spread() replaces variant matching
- ObjectPropertyKind::is_spread() replaces variant matching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add support for CommonJS config files:
- module.exports = { ... }
- module.exports = defineConfig({ ... })

Refactored shared config extraction into extract_config_from_expr,
used by both export default and module.exports paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace umbrella `oxc` dependency with `oxc_ast`, `oxc_parser`, and
`oxc_span` for more precise dependency tracking. Add README documenting
supported patterns, config resolution order, return type semantics, and
limitations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Value

Rewrite f64_to_json_number to follow JSON.stringify semantics using
serde_json's From<f64> for the NaN/Infinity→null fallback, and
i64::try_from for safe integer conversion. Rename StaticFieldValue to
FieldValue for brevity. Add tests for overflow-to-infinity and -0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@branchseer branchseer force-pushed the feat/add-tracing-instrumentation branch from 2036b4f to 087e05d Compare March 4, 2026 03:26
@branchseer branchseer changed the base branch from main to feat/static-config-extraction March 4, 2026 03:27
@branchseer branchseer force-pushed the feat/add-tracing-instrumentation branch from 031f50a to 7251d04 Compare March 4, 2026 04:36
@netlify
Copy link

netlify bot commented Mar 4, 2026

Deploy Preview for viteplus-staging failed.

Name Link
🔨 Latest commit 0f9cfd8
🔍 Latest deploy log https://app.netlify.com/projects/viteplus-staging/deploys/69b215e173ae730008d67446

…onfig file

Two improvements to static config extraction:

1. When no vite.config.* file exists in a workspace package,
   resolve_static_config now returns an empty map (instead of None).
   The caller sees no `run` field and returns immediately, skipping
   the NAPI/JS callback. This eliminates ~165ms cold Node.js init +
   ~3ms/pkg warm overhead for monorepo packages without config files.

2. Support defineConfig(fn) where fn is an arrow function or function
   expression. The extractor locates the return expression inside
   the function body and extracts fields from it. Functions with
   multiple return statements are rejected as not statically analyzable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@branchseer branchseer force-pushed the feat/static-config-extraction branch from 474fef7 to ecbafe2 Compare March 4, 2026 04:43
@branchseer branchseer force-pushed the feat/add-tracing-instrumentation branch from 7251d04 to 4be1fe4 Compare March 4, 2026 04:43
…r lookup

Handles the common pattern where the config object is assigned to a variable
before being exported or returned:

  const config = defineConfig({ ... });
  export default config;           // ESM indirect export
  module.exports = config;         // CJS indirect export
  return config;                   // inside defineConfig(fn) callback

Resolution scans top-level VariableDeclarator nodes by name (simple binding
identifiers only; destructured patterns are skipped). One level of indirection
is supported — chained references (const a = b; export default a) are not
resolved and fall back to NAPI as before.

Fixes tanstack-start-helloworld's ~1.3s config load time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@branchseer branchseer force-pushed the feat/add-tracing-instrumentation branch from 4be1fe4 to c7e8d4c Compare March 12, 2026 00:54
@branchseer branchseer force-pushed the feat/add-tracing-instrumentation branch from c7e8d4c to b6af764 Compare March 12, 2026 01:17
`{ a: 1, ...x, b: 2 }` — the spread may override `a`, so `a` is now
marked NonStatic. Fields declared after the spread (`b`) are unaffected
since they win over any spread key. Unknown keys from the spread are
still not added to the map.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@branchseer branchseer force-pushed the feat/add-tracing-instrumentation branch from b6af764 to 6f2ca96 Compare March 12, 2026 01:20
branchseer and others added 6 commits March 12, 2026 09:24
`{ a: 1, [key]: 2, b: 3 }` — `[key]` could resolve to `'a'` and
override it, so `a` is now marked NonStatic. Same logic as spreads.
Fields after the computed key are unaffected (they explicitly win).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Chrome DevTools timeline tracing output via `VITE_LOG_OUTPUT=chrome-json`
- Update `init_tracing()` to support three output modes: chrome-json, readable, stdout
- Add `VITE_LOG_OUTPUT` env var constant
- Add `tracing` and `tracing-chrome` dependencies to vite_shared
- Keep tracing guard alive in main() for Chrome trace file flushing
- Update vite-task dependency to include tracing instrumentation
- Enable tracing in e2e-test workflow and upload trace artifacts
- Run e2e commands twice to measure caching performance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The tracing crate is not directly used in vite_shared - only
tracing-chrome and tracing-subscriber are needed. Fixes cargo-shear.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instrument vite-plus callback implementations (handle_command,
load_user_config_file, resolve), NAPI bridge (run, JS resolvers,
vite config resolver), and JS startup timing for performance
measurement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use tracing::debug! events instead of spans in Send-required contexts
  (NAPI run function and vite config resolver)
- Fix cargo fmt formatting in tracing.rs
- Add process.uptime() to JS startup timing for absolute timeline

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
branchseer and others added 19 commits March 12, 2026 09:24
Update lock file to match vite-task rev 9ed02855, which adds tracing
dependencies and updates wax to 0.7.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The NAPI init() function was dropping the chrome-json tracing guard
immediately, resulting in empty trace files from the Node.js process.
Store it in a static OnceLock so it lives until process exit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OnceLock requires its inner type to be Sync for static storage,
but Box<dyn Any + Send> is not Sync. Wrap in Mutex to satisfy the bound.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rust statics stored in OnceLock are never dropped on process exit,
so the ChromeLayer FlushGuard never flushes trace data to disk.
Add an explicit shutdown_tracing() NAPI function that drops the guard,
and call it from bin.ts before process.exit().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Includes detailed timing analysis from Chrome tracing (tracing-chrome)
across global CLI, NAPI startup, vite-task session, and task execution
phases. Key finding: vite.config.ts loading via JS callbacks accounts
for 99% of the overhead before task execution (~930ms for 10-package
monorepo).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The new shutdownTracing() NAPI function needs its export registered
in the auto-generated binding files for the snapshot test to pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Align markdown table columns per oxfmt formatting rules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NAPI-RS generates type declarations alphabetically, placing
shutdownTracing (s) after runCommand (r) and its interfaces.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tters

Trace files (trace-*.json) written to the project cwd were picked up by
formatters during E2E tests, causing failures due to truncated JSON.

Add VITE_LOG_OUTPUT_DIR env var to write trace files to a dedicated
directory. Set it in the E2E workflow to ${{ runner.temp }}/trace-artifacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Expands from 2-project analysis to full 9-project ecosystem-ci coverage
(73 trace files). Adds cross-project comparison table, per-command
breakdown for frm-stack, config loading pattern analysis, and first-run
vs cache-run statistics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Enable `cacheScripts: true` in patch-project.ts after migration so
  `vp run` commands exercise the task cache hit/miss code paths.
- Add a third "cache miss" run to the e2e workflow that modifies
  package.json between runs to trigger PostRunFingerprintMismatch.

The e2e flow is now: first run (cache miss NotFound) → second run
(cache hit) → invalidate → third run (cache miss FingerprintMismatch).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migration preserves the existing config extension. Some projects use
vite.config.js, so we need to check both filenames.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Some projects (vue-mini, vitepress, frm-stack, dify) have no
lint/fmt/pack config to merge, so migration doesn't create a
vite config. Create a minimal one with cacheScripts enabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Projects like vue-mini run `vp run format` which checks Prettier
formatting. The injected cacheScripts config must be formatted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Projects like vue-mini have strict TypeScript-ESLint configs that reject
.ts files not included in tsconfig.json. Using .js avoids this issue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge architecture overview/phase breakdowns with cache-enabled trace
analysis (run #22558467033). Remove turbo references and optimization
suggestions. Add cache hit savings, operation overhead, execution
timeline, and miss root cause data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@branchseer branchseer force-pushed the feat/add-tracing-instrumentation branch from 6f2ca96 to 0f9cfd8 Compare March 12, 2026 01:24
@branchseer branchseer force-pushed the feat/static-config-extraction branch from 45787e4 to 9cce0f4 Compare March 12, 2026 01:38
branchseer added a commit that referenced this pull request Mar 12, 2026
Add new `vite_static_config` crate that uses `oxc_parser` to statically
extract JSON-serializable fields from `vite.config.*` files without
executing JavaScript

## Performance

Measured using Chrome trace instrumentation (PR #663).

On a cache hit, `vp run` loads the config, validates the fingerprint,
and streams cached output — no build work. Before this PR, config
loading required a NAPI round-trip to evaluate the config file. With
static extraction that cost drops to ~0ms.

### `vp run` end-to-end time (cache hit)

| Project | Before | After | Reduction |
|---------|--------|-------|-----------|
| vue-mini | ~217ms | **23ms** | ~89% |
| oxlint-plugin-complexity | ~220ms | **17ms** | ~92% |
| vitepress | ~282ms | **32ms** | ~89% |
| vite-vue-vercel | ~326ms | **28ms** | ~91% |
| rollipop | ~670ms | **16–53ms** | ~92–98% |
| frm-stack (10 packages) | ~895ms | **34–69ms** | ~92–96% |
| tanstack-start-helloworld | ~1,383ms | ~1,394ms | — (indirect export,
not yet handled) |

## Summary

- `VitePlusConfigLoader` now tries static extraction first for the `run`
config, falling back to NAPI-based resolution only when the config
cannot be statically extracted
- Static extraction supports all common patterns:
  - `export default { ... }` — bare object literal
  - `export default defineConfig({ ... })` — direct call
  - `export default defineConfig(() => ({ ... }))` — concise arrow body
  - `export default defineConfig(fn)` — block body with single return
  - `module.exports = ...` — CJS equivalents
- When no `vite.config.*` file exists, returns an empty map immediately
(skips NAPI entirely)
- Spread and computed-key properties correctly invalidate
previously-seen fields

## Test plan
- [x] 50 unit tests in `vite_static_config` pass
- [x] All existing `vite-plus-cli` tests pass
- [x] `cargo fmt --check` passes
- [x] `cargo shear` reports no unused dependencies
- [x] `cargo clippy` clean (no new warnings)
- [ ] CI passes on all platforms

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Base automatically changed from feat/static-config-extraction to main March 12, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test: e2e Auto run e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant