Skip to content

pipes: fix and improve the pipes inspector from the October 2026 audit #323

Closed as duplicate

Description

@erkamyaman

Context

This comes from a code audit of the pipes inspector (collector, runtime wrapper, source scan, lint, explain-pipe and the panel page) done during a comparison with other devtools in October 2026. Roadmap items live in #310, #311, #312 and #313. Items marked (to confirm) were not reproduced; every item below was checked against the code on main.

Problems

Medium

  • Pages mixed in the panel: the pipe-usage shared state is the aggregate of every connected page (counts summed, last call taken from the newest page, async rows flattened), and the panel applies it as is. The panel for tab A shows tab B's instances, calls and async rows with no page label, while recording targets hostPageId(). Publish per-page state (like pipePageFor does for agents) and filter by hostPageId(), or add a page picker. At least tag each async row and instance with its pageId (packages/devtools/src/rpc/pipes-tools.ts:150-222, app/src/pages/pipes-inspector.ts:1193-1205).
  • impure-pipe-in-for is noisy for built-ins: the purity map is seeded from BUILTIN_PIPES, where async, json, keyvalue and slice are impure, so item.price$ | async inside @for raises a warning although it is idiomatic and cheap. Exclude async (and json, which has its own rule) or downgrade built-ins to info, and add a test for @for { {{ x$ | async }} } (packages/devtools/src/rpc/pipe-lint.ts:62-63, 125-134).
  • No transform timing: the wrapper calls original.apply without measuring it, yet the explain-pipe tool description says it answers "why is this pipe slow?". Time each call with performance.now(), keep totalMs, maxMs and lastMs per instance, and report them in PipeCallInfo, the panel and explain-pipe (packages/devtools/src/pipes-runtime.ts:257-271, packages/devtools/src/devframe.ts:2664).
  • No usage sites for custom pipes: recordUsages skips any name not in BUILTIN_PIPES, and the panel shows "Used in templates" only when p.builtin. Your own pipes never get usage sites or the +N more link. Record usages for every name and attach usageCount and usages to custom PipeInfo entries (packages/devtools/src/rpc/get-pipes.ts:268-280, app/src/pages/pipes-inspector.ts:304).

Low

  • Thrown transforms are not recorded: original.apply runs before the try, so when transform throws, onCall never runs and a failing pipe looks like "no calls". Wrap it in try/catch, record an error count and the last (redacted) message, and rethrow (packages/devtools/src/pipes-runtime.ts:258).
  • Full source scan on every request: each explain-pipe call runs scanPipes and lintPipes, which both walk and read the whole tree and re-run templatesIn; the panel refresh runs get-pipes and pipe-lint too. Nothing is cached. Share one file and template index keyed on mtime and invalidate it from the Vite watcher (packages/devtools/src/rpc/pipe-explain.ts:37-39, packages/devtools/src/rpc/get-pipes.ts:97-130).
  • Own-property and subclass transforms: only proto.transform is patched, so a class-field arrow transform is never recorded. A subclass whose inherited transform is the parent's wrapper is skipped as already wrapped, and its calls are reported under the parent's slot.name. Patch the own property when there is one, and resolve the name from this (a WeakMap from instance to name filled by addPipe) instead of the closure (packages/devtools/src/pipes-runtime.ts:251-262).
  • Stale check silently inactive: staleCheckFor regex-scans tView.template.toString() and returns null in minified or mangled builds; the collector caches that null per instance and never says the check is off. Report staleCheck: 'unavailable' so the panel and explain-pipe can say so (packages/devtools/src/pipes-runtime.ts:335-361, packages/devtools/src/pipes-collector.ts:247-252).
  • False duplicate async subscriptions: duplicates are counted by source reference only, so a Subject, shareReplay stream or store.select used twice is flagged, and explain-pipe says the work repeats. Mark obviously shared sources as shared and show them as info (packages/devtools/src/pipes-collector.ts:120-125, packages/devtools/src/rpc/pipe-explain.ts:60).
  • Silent truncation: async rows stop at 200, component targets at 10 and per-instance calls at 5, and the merged instances are sliced again, with no flag in the payload. Add asyncTotal and truncated counts to PipePageReport and show "showing 200 of N" (packages/devtools/src/pipes-collector.ts:43-44, 133, 195, packages/devtools/src/rpc/pipes-tools.ts:198).
  • Per-instance rows lack the component: the payload carries component (and lastCaller) for each instance, but the panel renders only args, result and count, and its PipeInstanceCall type omits lastCaller. Show the component with a highlight target and the caller (app/src/pages/pipes-inspector.ts:46-51, 370-374).
  • call and stale are not validated: isPipeUsageInfo ignores call and stale, so a page can push any size of lastArgs, instances or lastCaller into shared state and agent markdown; capJson caps only at render time. Validate PipeCallInfo and clamp arrays and string lengths in isPipePageReport or mergePipePageReport (packages/devtools/src/rpc/pipes-tools.ts:106-117).
  • First calls of new pipe classes are missed: addPipe runs only inside pushPipes, so a pipe class that first appears while recording is not patched until the next push and its first calls are lost, while the docs say each recording starts from zero. Patch right after discovery in the mutation path, or document the delay (packages/devtools/src/pipes-collector.ts:493-495, apps/docs/src/content/inspectors/pipes.md:84).

Improvements

  • "Hot pipes" sort and view: calls per second and total ms per pipe, ranked, once timing exists (pipes-collector.ts, app/src/pages/pipes-inspector.ts).
  • Per-template pipe list in the Components inspector: selecting a component lists the pipes its template uses with live values, linked to the Pipes tab (component-tree.ts, app/src/pages/pipes-inspector.ts).
  • Migration suggestions as agent actions with a patch preview: a pure pipe reading signals becomes computed(), obs$ | async becomes toSignal() or @let, an impure custom pipe gets a memoized pure version (rpc/pipe-lint.ts, rpc/pipes-agent.ts, config.ts).
  • Detect identical-result churn: an impure pipe whose result is deep-equal on more than 95% of recorded calls should probably be pure (pipes-collector.ts, rpc/pipe-explain.ts).
  • Label library pipes by resolving the class to its node_modules package, in the list and in explain-pipe (pipes-collector.ts, rpc/pipe-explain.ts).
  • A pipe-usages agent resource that returns the usage sites of custom pipes, so agents can refactor every call site (rpc/pipes-agent.ts, rpc/get-pipes.ts, config.ts).

Already tracked elsewhere

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or shows wrong datafeatureA feature request or a pull request that adds one

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions