diff --git a/app/components/summary-item.hbs b/app/components/summary-item.hbs new file mode 100644 index 0000000000..31209b95ac --- /dev/null +++ b/app/components/summary-item.hbs @@ -0,0 +1,21 @@ + + + + <@list.cell class="list-cell-main js-render-main-cell js-render-profile-name" style={{this.nameStyle}}> + {{@model.name}} + + +<@list.cell class="list-cell-main js-render-main-cell list-cell-value_numeric js-render-profile-duration"> + {{@model.initial-render}} + + +<@list.cell class="list-cell-main js-render-main-cell list-cell-value_numeric js-render-profile-duration"> + {{@model.avg-re-render}} + + + <@list.cell class="list-cell-main js-render-main-cell list-cell-value_numeric js-render-profile-timestamp"> + {{@model.render-count}} + + + + diff --git a/app/components/summary-item.ts b/app/components/summary-item.ts new file mode 100644 index 0000000000..c3bcc0dc07 --- /dev/null +++ b/app/components/summary-item.ts @@ -0,0 +1,17 @@ +import Component from '@glimmer/component'; + +interface SummaryItemArgs { + model: { + name: string; + 'initial-render': number; + 'avg-re-render': number; + 'render-count': number; + }; + list: unknown; // or just leave this out +} + +export default class SummaryItem extends Component { + get row() { + return this.args.model; + } +} diff --git a/app/components/summary-render-table.hbs b/app/components/summary-render-table.hbs new file mode 100644 index 0000000000..203a4321e8 --- /dev/null +++ b/app/components/summary-render-table.hbs @@ -0,0 +1,10 @@ + + + {{#each this.rows as |row|}} + + {{/each}} + + diff --git a/app/components/summary-render-table.ts b/app/components/summary-render-table.ts new file mode 100644 index 0000000000..c3a4714312 --- /dev/null +++ b/app/components/summary-render-table.ts @@ -0,0 +1,88 @@ +import Component from '@glimmer/component'; + +import summarySchema from '../schemas/summary-render-tree'; + +import escapeRegExp from '../utils/escape-reg-exp'; + +import type { RenderTreeModel } from '../routes/render-tree'; + +import isEmpty from '@ember/utils/lib/is_empty'; + +interface SummaryRenderArgs { + profiles: RenderTreeModel['profiles']; + searchValue: string; +} + +// TODO handle for recursive cases also + +export default class SummaryRenderTable extends Component { + get schema() { + return summarySchema; + } + + get escapedSearch() { + return escapeRegExp(this.args.searchValue?.toLowerCase()); + } + + get rows() { + const profiles = this.args.profiles ?? []; + + if (profiles.length === 0) { + return []; + } + + // Flatten children (actual components) + const allComponents = profiles.flatMap((p) => p.children ?? []); + + const grouped: Record< + string, + { + initial: number | null; + reRenders: number[]; + } + > = {}; + + allComponents.forEach((profile) => { + const name = profile.name; + + const time = profile.time; // precise ms + + if (!grouped[name]) { + grouped[name] = { initial: null, reRenders: [] }; + } + + if (grouped[name].initial === null) { + // First time we see this component → initial render + grouped[name].initial = time; + } else { + // All later times → re-renders + grouped[name].reRenders.push(time); + } + }); + + return Object.entries(grouped) + .map(([name, data]) => { + const avgReRender = data.reRenders.length + ? data.reRenders.reduce((a, b) => a + b, 0) / data.reRenders.length + : 0; + + const count = data.reRenders.length + (data.initial ? 1 : 0); + return { + name, + 'initial-render': data.initial ? Number(data.initial.toFixed(2)) : 0, + 'avg-re-render': Number(avgReRender.toFixed(2)), + 'render-count': count, + }; + }) + .filter((item) => { + if (isEmpty(this.escapedSearch)) { + return true; + } + + const regExp = new RegExp(this.escapedSearch as string); + return !!item.name.toLowerCase().match(regExp); + }) + .sort((a, b) => b['initial-render'] - a['initial-render']) + .slice(0, 5); + } +} diff --git a/app/routes/render-tree.ts b/app/routes/render-tree.ts index 9ff1eae978..606d39ebba 100644 --- a/app/routes/render-tree.ts +++ b/app/routes/render-tree.ts @@ -10,6 +10,7 @@ import type RenderTreeController from '../controllers/render-tree'; import TabRoute from './tab'; export interface Profile { + time: number; children: Array; name: string; } diff --git a/app/schemas/summary-render-tree.ts b/app/schemas/summary-render-tree.ts new file mode 100644 index 0000000000..16d4101b77 --- /dev/null +++ b/app/schemas/summary-render-tree.ts @@ -0,0 +1,30 @@ +/** + * Summary render performance schema. + */ +export default { + columns: [ + { + id: 'name', + name: 'Component', + visible: true, + }, + { + id: 'initial-render', + name: 'Initial Render Time (ms)', + visible: true, + numeric: true, + }, + { + id: 'avg-re-render', + name: 'Avg Re-Render Time (ms)', + visible: true, + numeric: true, + }, + { + id: 'render-count', + name: 'Render Count', + visible: true, + numeric: true, + }, + ], +}; diff --git a/app/templates/render-tree.hbs b/app/templates/render-tree.hbs index 318b10d0cf..95835dd122 100644 --- a/app/templates/render-tree.hbs +++ b/app/templates/render-tree.hbs @@ -38,7 +38,10 @@ {{else}} - + + + {{!-- {{/each}} - + --}} {{/if}} \ No newline at end of file diff --git a/tests/integration/components/summary-item-test.ts b/tests/integration/components/summary-item-test.ts new file mode 100644 index 0000000000..9758f56b3d --- /dev/null +++ b/tests/integration/components/summary-item-test.ts @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-inspector/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | summary-item', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +}); diff --git a/tests/integration/components/summary-render-table-test.ts b/tests/integration/components/summary-render-table-test.ts new file mode 100644 index 0000000000..7fd9ec3e1e --- /dev/null +++ b/tests/integration/components/summary-render-table-test.ts @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-inspector/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | summary-render-table', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +});