Skip to content

Commit 38fbae3

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/pre-release-cleanup
# Conflicts: # apps/docs/src/content/roadmap.md # extension/ui/assets/browser-agent-rpc-KH4HHvHf-CUsc0F99.js # extension/ui/assets/browser-agent-rpc-KH4HHvHf-Da157Vn1.js # extension/ui/assets/browser-agent-rpc-KH4HHvHf-G6FmeCEh.js # extension/ui/assets/index-B6W3bI5a.js # extension/ui/assets/index-BpSgirhF.js # extension/ui/assets/index-DPr65zhW.js # extension/ui/index.html
2 parents 7326cad + 07e5fab commit 38fbae3

31 files changed

Lines changed: 1801 additions & 204 deletions

‎app/src/__tests__/performance-build-pages.test.ts‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import {
77
perfPages,
88
type PerfPage,
99
} from '../pages/performance-inspector';
10-
import { BuildInspector, formatBytes, type BuildGraph } from '../pages/build-inspector';
10+
import {
11+
BuildInspector,
12+
formatBytes,
13+
formatMs,
14+
timeBreakdown,
15+
type BuildGraph,
16+
} from '../pages/build-inspector';
1117

1218
function client(options: { state?: unknown; call?: (name: string) => unknown }) {
1319
const call = vi.fn(async (name: string) => options.call?.(name));
@@ -172,4 +178,58 @@ describe('Build tab', () => {
172178
it('formats bytes', () => {
173179
expect(formatBytes(4200)).toBe('4.2 kB');
174180
});
181+
182+
it('shows the time per module and sorts the module list by time', async () => {
183+
const timed: BuildGraph = {
184+
...graph,
185+
vite: {
186+
...graph.vite!,
187+
modules: graph.vite!.modules.map((mod, i) =>
188+
i === 0 ? mod : { ...mod, time: { resolve: 0.2, load: 1, transform: i, total: i + 1.2 } },
189+
),
190+
timings: { modules: 29, capped: false },
191+
},
192+
};
193+
const { host } = await render(timed);
194+
const table = () => host.querySelectorAll('section')[0]!.querySelectorAll('table')[1]!;
195+
const firstCells = () =>
196+
[...table().querySelectorAll('tbody tr')].slice(0, 2).map((row) => {
197+
const cells = row.querySelectorAll('td');
198+
return [cells[0]!.textContent!.trim(), cells[2]!.textContent!.trim()];
199+
});
200+
expect(host.textContent).toContain('Time is what the dev server spent on 29 modules');
201+
// Biggest first, as the server sends them; m0 was not timed.
202+
expect(firstCells()).toEqual([
203+
['src/m0.ts', ''],
204+
['src/m1.ts', '2.2 ms'],
205+
]);
206+
expect(table().querySelectorAll('tbody tr td')[7]!.getAttribute('title')).toBe(
207+
'Resolve 0.2 ms, load 1.0 ms, transform 1.0 ms',
208+
);
209+
const timeHeader = [...table().querySelectorAll('th')].find((th) =>
210+
th.textContent!.includes('Time'),
211+
)!;
212+
const sortButton = timeHeader.querySelector('button')!;
213+
sortButton.click();
214+
await fixture.whenStable();
215+
expect(timeHeader.getAttribute('aria-sort')).toBe('ascending');
216+
expect(firstCells()[0]).toEqual(['src/m1.ts', '2.2 ms']);
217+
sortButton.click();
218+
await fixture.whenStable();
219+
expect(timeHeader.getAttribute('aria-sort')).toBe('descending');
220+
expect(firstCells()[0]).toEqual(['src/m29.ts', '30.2 ms']);
221+
// The module without a time stays last in both directions.
222+
[...host.querySelectorAll('button')].find((b) => b.textContent!.includes('Show all'))!.click();
223+
await fixture.whenStable();
224+
const rows = table().querySelectorAll('tbody tr');
225+
expect(rows[rows.length - 1]!.querySelector('td')!.textContent!.trim()).toBe('src/m0.ts');
226+
});
227+
228+
it('formats milliseconds and the time breakdown', () => {
229+
expect(formatMs(12.34)).toBe('12.3 ms');
230+
expect(formatMs(2500)).toBe('2.50 s');
231+
expect(timeBreakdown({ resolve: 0.1, load: 2, transform: 40.5, total: 42.6 })).toBe(
232+
'Resolve 0.1 ms, load 2.0 ms, transform 40.5 ms',
233+
);
234+
});
175235
});

‎app/src/pages/build-inspector.ts‎

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Component, computed, effect, input, signal } from '@angular/core';
22
import type { DevframeRpcClient } from 'devframe/client';
33
import { SearchField } from '../ui/search-field';
4+
import { SortHeader, sortRows, type SortState } from '../ui/sort-header';
45

56
/** One module of the Vite dev server's graph, as `build-graph.ts` sends it. */
67
export interface GraphModule {
@@ -11,6 +12,8 @@ export interface GraphModule {
1112
importers: number;
1213
imports: number;
1314
dynamicImports: string[];
15+
/** Wall-clock milliseconds the dev server spent on it, when the Vite plugin timed it. */
16+
time?: { resolve: number; load: number; transform: number; total: number };
1417
}
1518

1619
export interface LazyBoundary {
@@ -38,6 +41,7 @@ export interface BuildGraph {
3841
entries: string[];
3942
initialSize: number;
4043
lazy: LazyBoundary[];
44+
timings?: { modules: number; capped: boolean };
4145
};
4246
viteNote?: string;
4347
output?: {
@@ -59,12 +63,22 @@ export function formatBytes(bytes: number): string {
5963
return `${(bytes / 1_000_000).toFixed(2)} MB`;
6064
}
6165

66+
/** `12.34` as `12.3 ms`, `2500` as `2.50 s`. */
67+
export function formatMs(ms: number): string {
68+
return ms < 1000 ? `${ms.toFixed(1)} ms` : `${(ms / 1000).toFixed(2)} s`;
69+
}
70+
71+
/** What the dev server spent on a module, phase by phase. */
72+
export function timeBreakdown(time: NonNullable<GraphModule['time']>): string {
73+
return `Resolve ${formatMs(time.resolve)}, load ${formatMs(time.load)}, transform ${formatMs(time.transform)}`;
74+
}
75+
6276
/** Rows shown per table before **Show all**. */
6377
export const BUILD_ROWS = 25;
6478

6579
@Component({
6680
selector: 'app-build-inspector',
67-
imports: [SearchField],
81+
imports: [SearchField, SortHeader],
6882
template: `
6983
<p class="intro">
7084
What the app loads: the Vite dev server's module graph with its lazy boundaries, and the
@@ -90,6 +104,14 @@ export const BUILD_ROWS = 25;
90104
only what the browser requested). {{ bytes(vite.initialSize) }} load without
91105
<code>import()</code> from {{ vite.entries.length }}
92106
{{ vite.entries.length === 1 ? 'entry' : 'entries' }}.
107+
@if (vite.timings; as timings) {
108+
Time is what the dev server spent on {{ timings.modules }}
109+
{{ timings.modules === 1 ? 'module' : 'modules' }}, wall clock: resolving imports to
110+
it, and its last load and transform through all plugins.
111+
@if (timings.capped) {
112+
The oldest timings were dropped.
113+
}
114+
}
93115
</p>
94116
<h3>Lazy boundaries</h3>
95117
@if (!vite.lazy.length) {
@@ -149,14 +171,19 @@ export const BUILD_ROWS = 25;
149171
<div class="scroll">
150172
<table>
151173
<caption class="sr-only">
152-
Modules, biggest first
174+
Modules, biggest first unless sorted
153175
</caption>
154176
<thead>
155177
<tr>
156-
<th scope="col">Module</th>
157-
<th scope="col" class="num">Size</th>
158-
<th scope="col" class="num">Importers</th>
159-
<th scope="col" class="num">Imports</th>
178+
<th scope="col" appSortHeader="module" [(sort)]="moduleSort">Module</th>
179+
<th scope="col" class="num" appSortHeader="size" [(sort)]="moduleSort">Size</th>
180+
<th scope="col" class="num" appSortHeader="time" [(sort)]="moduleSort">Time</th>
181+
<th scope="col" class="num" appSortHeader="importers" [(sort)]="moduleSort">
182+
Importers
183+
</th>
184+
<th scope="col" class="num" appSortHeader="imports" [(sort)]="moduleSort">
185+
Imports
186+
</th>
160187
</tr>
161188
</thead>
162189
<tbody>
@@ -166,6 +193,11 @@ export const BUILD_ROWS = 25;
166193
<code>{{ row.file ?? row.url }}</code>
167194
</td>
168195
<td class="num">{{ bytes(row.size) }}</td>
196+
@if (row.time; as time) {
197+
<td class="num" [title]="breakdown(time)">{{ ms(time.total) }}</td>
198+
} @else {
199+
<td class="num"></td>
200+
}
169201
<td class="num">{{ row.importers }}</td>
170202
<td class="num">{{ row.imports }}</td>
171203
</tr>
@@ -347,20 +379,30 @@ export class BuildInspector {
347379
readonly rpc = input<DevframeRpcClient | null>(null);
348380

349381
protected readonly bytes = formatBytes;
382+
protected readonly ms = formatMs;
383+
protected readonly breakdown = timeBreakdown;
350384

351385
readonly graph = signal<BuildGraph | null>(null);
352386
readonly loading = signal(false);
353387
readonly failed = signal(false);
354388
readonly status = signal('');
355389
readonly filter = signal('');
356390
readonly allModules = signal(false);
391+
readonly moduleSort = signal<SortState | null>(null);
357392

358393
readonly shownModules = computed(() => {
359394
const modules = this.graph()?.vite?.modules ?? [];
360395
const text = this.filter().trim().toLowerCase();
361-
return text
396+
const matching = text
362397
? modules.filter((mod) => `${mod.url} ${mod.file ?? ''}`.toLowerCase().includes(text))
363398
: modules;
399+
return sortRows(matching, this.moduleSort(), {
400+
module: (mod) => mod.file ?? mod.url,
401+
size: (mod) => mod.size,
402+
time: (mod) => mod.time?.total,
403+
importers: (mod) => mod.importers,
404+
imports: (mod) => mod.imports,
405+
});
364406
});
365407
readonly visibleModules = computed(() =>
366408
this.allModules() ? this.shownModules() : this.shownModules().slice(0, BUILD_ROWS),

‎apps/docs/src/content/agents/tools.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,10 +1055,10 @@ A metric the browser doesn't measure, or has no entry for yet, says `none yet`,
10551055

10561056
These tools read what the app loads. See [Build](../inspectors/build.md).
10571057

1058-
| Tool | What it answers | Arguments |
1059-
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
1060-
| `module-graph` | The Vite dev server's module graph: the entries, the lazy boundaries (`import()`) with the modules and size each pulls in, and the biggest modules. With `search`, matching modules with their importers, imports and `import()` calls. | `search`, `limit` |
1061-
| `chunk-sizes` | The chunks of the last production build: initial or lazy, size, gzip size, and with `stats.json` the entry point. | `limit` |
1058+
| Tool | What it answers | Arguments |
1059+
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
1060+
| `module-graph` | The Vite dev server's module graph: the entries, the lazy boundaries (`import()`) with the modules and size each pulls in, the biggest modules, and the slowest modules with the wall-clock time Vite spent resolving, loading and transforming each. With `search`, matching modules with their size, time, importers, imports and `import()` calls. | `search`, `limit` |
1061+
| `chunk-sizes` | The chunks of the last production build: initial or lazy, size, gzip size, and with `stats.json` the entry point. | `limit` |
10621062

10631063
`module-graph` needs the Vite plugin, in the dev server's process; it reads nothing on the stdio MCP server. Sizes are transformed, unminified code, for modules the browser requested. `chunk-sizes` reads the build output on disk, so it works anywhere the project is, and says how to build when there is none. `limit` defaults to 20 for `module-graph` (at most 200) and 30 for `chunk-sizes` (at most 500).
10641064

0 commit comments

Comments
 (0)