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
15 changes: 15 additions & 0 deletions app/src/__tests__/cd-recording-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,19 @@ describe('CdRecording status', () => {
expect(live()).toBe('Could not reach the page.');
expect(live()).not.toContain('Cleared');
});

it('notes the classes and instances a recording checked past its caps', async () => {
expect(host().querySelectorAll('.note')).toHaveLength(0);
fixture.componentRef.setInput('page', {
...page(false, 1),
droppedClasses: 905,
droppedHosts: 1,
});
await fixture.whenStable();
const notes = [...host().querySelectorAll('.note')].map((n) => n.textContent!.trim());
expect(notes).toEqual([
'905 more component classes were checked than a recording keeps, so they are left out of these totals.',
'1 component instance was checked past the 2000 a recording counts, so its tree row shows no check count.',
]);
});
});
34 changes: 34 additions & 0 deletions app/src/__tests__/component-tree-defer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,38 @@ describe('defer blocks', () => {
await fixture.whenStable();
expect(row.textContent).toContain('Unknown component');
});

it('shows a + on the count when the page has more blocks than it reported', async () => {
const block = (id: string) => ({
id,
state: 'complete',
hydration: 'not-configured',
triggers: [],
rootIds: [],
});
const page = (deferTruncated?: boolean) => ({
pageId: 'p1',
roots: [{ id: 'c1', name: 'App', tag: 'app-root', children: [] }],
count: 1,
detail: null,
reportedAt: 1000,
deferBlocks: [block('d1'), block('d2')],
...(deferTruncated ? { deferTruncated } : {}),
});
const pill = async (value: unknown) => {
const fixture = TestBed.createComponent(ComponentTree);
fixture.componentRef.setInput('rpc', client({ p1: value }));
for (let i = 0; i < 3; i++) {
await new Promise((resolve) => setTimeout(resolve));
await fixture.whenStable();
}
const text = (fixture.nativeElement as HTMLElement)
.querySelector('#ct-defer-title .pill')!
.textContent!.trim();
fixture.destroy();
return text;
};
expect(await pill(page())).toBe('2');
expect(await pill(page(true))).toBe('2+');
});
});
20 changes: 20 additions & 0 deletions app/src/pages/cd-recording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const SHOWN_CYCLES = 30;
what="cycles"
limit="cdCycles"
/>
@for (note of capNotes(); track note) {
<p class="note">{{ note }}</p>
}
@if (slowest().length) {
<div class="cd-grid">
<div>
Expand Down Expand Up @@ -253,6 +256,23 @@ export class CdRecording {
[...(this.page()?.components ?? [])].sort((a, b) => b.ms - a.ms).slice(0, SHOWN),
);
readonly latest = computed(() => this.cycles().slice(-SHOWN_CYCLES).reverse());
/** Says when the recording checked more classes or instances than it keeps. */
readonly capNotes = computed(() => {
const classes = this.page()?.droppedClasses ?? 0;
const hosts = this.page()?.droppedHosts ?? 0;
const notes: string[] = [];
if (classes > 0) {
notes.push(
`${classes} more component ${classes === 1 ? 'class was' : 'classes were'} checked than a recording keeps, so ${classes === 1 ? 'it is' : 'they are'} left out of these totals.`,
);
}
if (hosts > 0) {
notes.push(
`${hosts} component ${hosts === 1 ? 'instance was' : 'instances were'} checked past the 2000 a recording counts, so ${hosts === 1 ? 'its tree row shows' : 'their tree rows show'} no check count.`,
);
}
return notes;
});
readonly status = computed(() => {
if (this.message()) return this.message();
const count = this.cycles().length;
Expand Down
9 changes: 8 additions & 1 deletion app/src/pages/component-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ interface Page {
title?: string;
platform?: string;
deferBlocks?: DeferBlock[];
/** The page has more defer blocks than the ones in `deferBlocks`. */
deferTruncated?: boolean;
roots: LiveNode[];
count: number;
truncated?: boolean;
Expand Down Expand Up @@ -516,7 +518,8 @@ function bare(name: string): string {
@if (deferBlocks().length) {
<section class="defer" aria-labelledby="ct-defer-title">
<h2 id="ct-defer-title" class="defer-title">
Defer blocks <span class="pill">{{ deferBlocks().length }}</span>
Defer blocks
<span class="pill">{{ deferCount() }}</span>
</h2>
<ul class="defer-list">
@for (block of deferBlocks(); track block.id) {
Expand Down Expand Up @@ -1469,6 +1472,10 @@ export class ComponentTree {
private readonly filtered = computed(() => filterTree(this.page()?.roots ?? [], this.query()));

readonly deferBlocks = computed(() => this.page()?.deferBlocks ?? []);
/** The block count, with a `+` when the page has more blocks than it reported. */
readonly deferCount = computed(
() => `${this.deferBlocks().length}${this.page()?.deferTruncated ? '+' : ''}`,
);

readonly countLabel = computed(() =>
countText(
Expand Down
7 changes: 4 additions & 3 deletions app/src/pages/di-inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,10 @@ function isTree(value: unknown): value is InjectorNode[] {
<path d="M12 11v5M12 8h.01" />
</svg>
<span>
<strong>This page has more than 2000 element injectors.</strong>
Only the first 2000 are shown, so a lookup path or a provider can point to one that is
not listed.
<strong>The page reported only part of its injector tree.</strong>
It keeps at most 2000 element injectors and 2500 in all, and leaves out injectors nested
more than 1000 levels deep, so a lookup path or a provider can point to one that is not
listed.
</span>
</p>
}
Expand Down
5 changes: 3 additions & 2 deletions app/src/pages/route-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { SHARED_STYLES, type RouterPage } from './router-types';
@if (page().setup; as setup) {
@if (setup.mode === 'events-only') {
<p class="note" role="note">
Events-only mode: this build has no debug utils (production build or unusual setup), so
the route config, lint and actions are limited.
Events-only mode: this build lacks the injector provider debug helper, so setup detection
is limited. The preloader, the scroller and the router providers can't be found, and the
features and options below can show defaults.
</p>
}
<dl class="facts">
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/content/agents/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ The signal graph of each page, under `pages[pageId]`. `graph` is the latest one.

### injector-tree

The injector hierarchy the page last reported, with the providers at each level. Each page is under `pages[pageId]`. `roots` and `environment` are the latest. Element injectors list what their components and directives inject in `dependencies`. Environment injectors list what the services they already created inject, with `from` naming the service or `InjectionToken`. `zone` is the change detection mode: `zoneless`, `zone` (zone.js) or `zone-unused` (zoneless, with zone.js still loaded). `truncated` is `true` when the page has more than 2000 element injectors and reported only the first 2000, or more than 2500 injectors in all. The server drops malformed nodes and clips long names.
The injector hierarchy the page last reported, with the providers at each level. Each page is under `pages[pageId]`. `roots` and `environment` are the latest. Element injectors list what their components and directives inject in `dependencies`. Environment injectors list what the services they already created inject, with `from` naming the service or `InjectionToken`. `zone` is the change detection mode: `zoneless`, `zone` (zone.js) or `zone-unused` (zoneless, with zone.js still loaded). `truncated` is `true` when the tree was cut: it keeps at most 2000 element injectors and 2500 injectors in all, and leaves out injectors nested more than 1000 levels deep. The server drops malformed nodes and clips long names.

### ngrx-store

Expand Down
6 changes: 3 additions & 3 deletions apps/docs/src/content/agents/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ The `@defer` blocks of each page: the component that holds each one, its state,

Without a development build, the page has no util to read defer blocks, and the answer says so.

Each page lists at most 50 blocks in the table and 50 under **Needs attention**, with a note on how many more are not shown. The whole answer stops at 20,000 characters. If several tabs report, pass `page` to read one. The **Defer blocks** list on the [Components](../inspectors/components.md#defer-blocks) page has every block.
Each page lists at most 50 blocks in the table and 50 under **Needs attention**, with a note on how many more are not shown. The whole answer stops at 20,000 characters. If several tabs report, pass `page` to read one. The **Defer blocks** list on the [Components](../inspectors/components.md#defer-blocks) page has every block the page reported. A page reports its first 500 blocks. When it has more, the answer says so.

### change-detection <ngmd-badge variant="alpha">Action</ngmd-badge>

Expand All @@ -145,7 +145,7 @@ Change detection cycles recorded with Angular's profiler: the slowest components
| `record` | no | `start` starts a fresh recording on a page that is not recording and leaves a running one as it is, `stop` stops it and keeps the cycles, `clear` empties it. Leave out to read. |
| `limit` | no | Rows per list. Default 10, at most 50. |

Without `page`, it reads the page that is recording, and `record` goes to every connected page. A `page` that names no connected tab gets the list of connected tabs. A connected tab that has not recorded yet gets a hint to start a recording there. The answer starts with the change detection mode of the page it reports on: zoneless, zone.js, or zoneless with zone.js still loaded. Recording is off until the panel or this tool starts it. Call it with `record: "start"`, use the app, then call it again without `record`. With `agent.readOnly`, the tool has no `record` argument and is marked read-only, so start the recording from the panel. When older cycles were dropped at [`limits.cdCycles`](../getting-started/configuration.md#limits), the answer says how many.
Without `page`, it reads the page that is recording, and `record` goes to every connected page. A `page` that names no connected tab gets the list of connected tabs. A connected tab that has not recorded yet gets a hint to start a recording there. The answer starts with the change detection mode of the page it reports on: zoneless, zone.js, or zoneless with zone.js still loaded. Recording is off until the panel or this tool starts it. Call it with `record: "start"`, use the app, then call it again without `record`. With `agent.readOnly`, the tool has no `record` argument and is marked read-only, so start the recording from the panel. When older cycles were dropped at [`limits.cdCycles`](../getting-started/configuration.md#limits), the answer says how many. A recording lists at most 100 component classes and counts checks for up to 2000 instances. When it checked more, the answer says how many were left out.

### inspect-signals

Expand Down Expand Up @@ -187,7 +187,7 @@ The injectors a page reported. Element injectors list what each component inject

A `selector` that names an element injector returns it with its lookup path resolved to names and provided tokens. One that names an environment injector, in any case, returns that injector's providers, what the services it already created inject, and its parent injectors. When nothing matches, the answer lists the tags and environment injector names on the page to retry with.

Without `selector` or `token`, the answer is the whole tree and says which change detection mode the page runs. The answer stays within 20,000 characters and its JSON always parses. A larger tree shows element injectors as an outline (id, name, type, component, and provider and dependency counts), and when that is still too long it leaves out the deepest injectors and says how many. The environment injectors are always there, as an outline when they are large, and without their deepest injectors when even that outline is too long. A `selector` or `token` answer that is too long keeps whole entries and says how many it shows. `selector` and `token` take at most 500 characters each. When the page has more than 2000 element injectors, the answer says that it holds only the first 2000.
Without `selector` or `token`, the answer is the whole tree and says which change detection mode the page runs. The answer stays within 20,000 characters and its JSON always parses. A larger tree shows element injectors as an outline (id, name, type, component, and provider and dependency counts), and when that is still too long it leaves out the deepest injectors and says how many. The environment injectors are always there, as an outline when they are large, and without their deepest injectors when even that outline is too long. A `selector` or `token` answer that is too long keeps whole entries and says how many it shows. `selector` and `token` take at most 500 characters each. When the page reported only part of its injector tree (it keeps at most 2000 element injectors and 2500 in all, and leaves out injectors nested more than 1000 levels deep), the answer says so.

## NgRx

Expand Down
4 changes: 4 additions & 0 deletions apps/docs/src/content/inspectors/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ When the page renders `@defer` blocks, a **Defer blocks** list follows the tree.

Hover or focus a row to highlight the block in the page. Click it to select the component that holds it. The list is hidden when the page renders no defer blocks, or when its Angular version has no util to read them.

The list holds the first 500 blocks of a page. When the page has more, the count reads **500+**.

### Change detection

Below the tree and the defer blocks, the **Change detection** section records change detection cycles with Angular's profiler. Click **Record**, use the app, then click **Stop recording**. **Clear** empties the recording. Each new recording starts empty, but a start sent while the page records, such as an agent's `record: "start"`, keeps the cycles recorded so far.
Expand All @@ -91,6 +93,8 @@ Below the tree and the defer blocks, the **Change detection** section records ch
- **Latest cycles**, newest first: the start time, the duration, how many components Angular checked, how many sync passes it ran, the output listener that ran just before the cycle (an output that ended more than 50 ms before the cycle is not counted), and the slowest component in it.
- Tree rows show how many times each instance was checked while recording.

**Slowest components** shows the 100 component classes with the most self time among the first 1000 classes a recording checks. A class first checked after those 1000 has no totals, so it isn't ranked. Tree rows count checks for up to 2000 instances. When a recording checks more classes or instances, a note in the section says how many were left out.

Recording is off by default. While it is off, the page registers no profiler for it. The page keeps the last 200 cycles, set with [`limits.cdCycles`](../getting-started/configuration.md#limits). Recording needs Angular 20 or later. On older versions, the section says so. Times come from a development build, so they are higher than in production.

### Source mode
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/content/inspectors/injectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ See [Tools](../agents/tools.md) and [Resources](../agents/resources.md).

### Up to 2000 element injectors

The page reports at most 2000 element injectors. Past that, a notice above the tree says so, and a lookup path or a provider can point to an injector that isn't listed. `inspect-providers` says it too. Environment injectors are kept after them, up to 2500 injectors in all, so they never push element injectors out.
The page reports at most 2000 element injectors. Environment injectors are kept after them, up to 2500 injectors in all, so they never push element injectors out. Injectors nested more than 1000 levels deep are left out too. When any of these limits cuts the tree, a notice above the tree says the page reported only part of its injector tree, and a lookup path or a provider can point to an injector that isn't listed. `inspect-providers` says it too.

### Some injectors can't be highlighted

Expand Down
4 changes: 4 additions & 0 deletions apps/docs/src/content/inspectors/pipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ It runs only while recording. It reads the template source, so it needs an unmin

`signal-read-in-pure-pipe` can't tell a signal on an injected service from a method with no arguments, so it reports those calls at info level.

### Highlighting keeps 10 hosts per component

A component chip keeps up to 10 host elements per page to highlight. When a component has more, `pangular:pipe-usage` gives the number of distinct host elements in `targetTotal`. A report that combines pages keeps at most 10 host elements per component across all pages, and its `targetTotal` adds up the host elements of every page.

### Recording ends on reload

Recording is off by default. Reload the page and it is off again.
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/content/inspectors/router.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ The overlay reads the helper `provideRouter()` publishes (`ng.ɵgetRouterInstanc

The live views need `window.ng`, so they need a development build. In a production build the overlay finds no Router, and **Current** says **This page reports no Router**.

When the debug API exists but lacks the provider helpers, the tab runs in events-only mode. The **Setup** view says so, and the config, lint and actions are limited.
When the debug API exists but lacks the provider helper (`ng.ɵgetInjectorProviders`), the tab runs in events-only mode. The route config is still read, so the tree and lint work. Setup detection is limited: the overlay can't find the preloader, the scroller or the router providers, so the **Setup** view says so and its features and options can show defaults.

### Guard verdicts

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading