diff --git a/bin/benchmark/run.mjs b/bin/benchmark/run.mjs index aadcea3197a..e823f93626b 100644 --- a/bin/benchmark/run.mjs +++ b/bin/benchmark/run.mjs @@ -37,9 +37,9 @@ const DEFAULT_MARKERS = [ 'clearItems1', 'render1000Items2', 'clearItems2', - 'render5000Items1', + 'render10000Items1', 'clearManyItems1', - 'render5000Items2', + 'render10000Items2', 'clearManyItems2', 'render1000Items3', 'append1000Items1', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb109a206f1..af1b9c903a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2939,9 +2939,6 @@ importers: ember-source: specifier: workspace:* version: link:../.. - ember-strict-application-resolver: - specifier: ^0.1.1 - version: 0.1.1(@babel/core@7.29.0) vite: specifier: ^7.3.0 version: 7.3.2(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1) @@ -7770,9 +7767,6 @@ packages: resolution: {integrity: sha512-89oVHVJwmLDvGvAUWgS87KpBoRhy3aZ6U0Ql6HOmU4TrPkyaa8pM0W81wj9cIwjYprcQtN9EwzZMHnq46+oUyw==} engines: {node: 8.* || 10.* || >= 12} - ember-strict-application-resolver@0.1.1: - resolution: {integrity: sha512-/49JZ2Yk2ggicAGIoFNWcz05llhe2i+/2dpH5Pv1KlwZOBKkqMMJpTKEB4IMg+FjKBiE0r2yxuZzo9Oly9vc1A==} - ember-template-imports@3.4.2: resolution: {integrity: sha512-OS8TUVG2kQYYwP3netunLVfeijPoOKIs1SvPQRTNOQX4Pu8xGGBEZmrv0U1YTnQn12Eg+p6w/0UdGbUnITjyzw==} engines: {node: 12.* || >= 14} @@ -18257,14 +18251,6 @@ snapshots: transitivePeerDependencies: - supports-color - ember-strict-application-resolver@0.1.1(@babel/core@7.29.0): - dependencies: - '@embroider/addon-shim': 1.10.2 - decorator-transforms: 2.3.1(@babel/core@7.29.0) - transitivePeerDependencies: - - '@babel/core' - - supports-color - ember-template-imports@3.4.2: dependencies: babel-import-util: 0.2.0 diff --git a/smoke-tests/benchmark-app/app/app.js b/smoke-tests/benchmark-app/app/app.js deleted file mode 100644 index 98252c239e2..00000000000 --- a/smoke-tests/benchmark-app/app/app.js +++ /dev/null @@ -1,9 +0,0 @@ -import Application from 'ember-strict-application-resolver'; - -export default class App extends Application { - modules = { - ...import.meta.glob('./router.*', { eager: true }), - ...import.meta.glob('./templates/application.*', { eager: true }), - ...import.meta.glob('./services/state.*', { eager: true }), - }; -} diff --git a/smoke-tests/benchmark-app/app/config/environment.js b/smoke-tests/benchmark-app/app/config/environment.js deleted file mode 100644 index 8530468c0fd..00000000000 --- a/smoke-tests/benchmark-app/app/config/environment.js +++ /dev/null @@ -1,10 +0,0 @@ -const ENV = { - modulePrefix: 'benchmark-app', - environment: import.meta.env.DEV ? 'development' : 'production', - rootURL: '/', - locationType: 'history', - EmberENV: {}, - APP: {}, -}; - -export default ENV; diff --git a/smoke-tests/benchmark-app/app/router.js b/smoke-tests/benchmark-app/app/router.js deleted file mode 100644 index ce92066c976..00000000000 --- a/smoke-tests/benchmark-app/app/router.js +++ /dev/null @@ -1,9 +0,0 @@ -import EmberRouter from '@embroider/router'; -import config from 'benchmark-app/config/environment'; - -export default class Router extends EmberRouter { - location = config.locationType; - rootURL = config.rootURL; -} - -Router.map(function () {}); diff --git a/smoke-tests/benchmark-app/index.html b/smoke-tests/benchmark-app/index.html index d9a7949c5ca..c0ee6cc822a 100644 --- a/smoke-tests/benchmark-app/index.html +++ b/smoke-tests/benchmark-app/index.html @@ -6,18 +6,18 @@ - + - + diff --git a/smoke-tests/benchmark-app/package.json b/smoke-tests/benchmark-app/package.json index c5d26558137..05021b791a0 100644 --- a/smoke-tests/benchmark-app/package.json +++ b/smoke-tests/benchmark-app/package.json @@ -31,7 +31,6 @@ "babel-plugin-ember-template-compilation": "^4.0.0", "decorator-transforms": "^2.3.1", "ember-source": "workspace:*", - "ember-strict-application-resolver": "^0.1.1", "vite": "^7.3.0" }, "engines": { diff --git a/smoke-tests/benchmark-app/app/templates/application.gjs b/smoke-tests/benchmark-app/src/app.gjs similarity index 67% rename from smoke-tests/benchmark-app/app/templates/application.gjs rename to smoke-tests/benchmark-app/src/app.gjs index 09077e00392..cf51d0dd1ba 100644 --- a/smoke-tests/benchmark-app/app/templates/application.gjs +++ b/smoke-tests/benchmark-app/src/app.gjs @@ -1,5 +1,5 @@ -import { TheTable } from './the-table.gjs'; -import { Jumbotron } from './jumbotron.gjs'; +import { TheTable } from './jsfb/the-table.gjs'; +import { Jumbotron } from './jsfb/jumbotron.gjs'; \ No newline at end of file + diff --git a/smoke-tests/benchmark-app/app/services/cell.js b/smoke-tests/benchmark-app/src/jsfb/cell.js similarity index 100% rename from smoke-tests/benchmark-app/app/services/cell.js rename to smoke-tests/benchmark-app/src/jsfb/cell.js diff --git a/smoke-tests/benchmark-app/app/templates/jumbotron.gjs b/smoke-tests/benchmark-app/src/jsfb/jumbotron.gjs similarity index 100% rename from smoke-tests/benchmark-app/app/templates/jumbotron.gjs rename to smoke-tests/benchmark-app/src/jsfb/jumbotron.gjs diff --git a/smoke-tests/benchmark-app/app/services/state.js b/smoke-tests/benchmark-app/src/jsfb/state.js similarity index 87% rename from smoke-tests/benchmark-app/app/services/state.js rename to smoke-tests/benchmark-app/src/jsfb/state.js index 3b1249018ae..4a4f977d9c9 100644 --- a/smoke-tests/benchmark-app/app/services/state.js +++ b/smoke-tests/benchmark-app/src/jsfb/state.js @@ -1,10 +1,9 @@ -import Service from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { trackedArray } from '@ember/reactive/collections'; -import { run, runLots, add, update, swapRows, deleteRow } from './utils.js'; +import { run, runLots, add, update, swapRows } from './utils.js'; -export default class State extends Service { +export class State { data = new trackedArray(); id = 1; @tracked selected; @@ -60,4 +59,4 @@ export default class State extends Service { isSelected = ({ id }) => { return this.selected === id; }; -} \ No newline at end of file +} diff --git a/smoke-tests/benchmark-app/app/templates/the-table.gjs b/smoke-tests/benchmark-app/src/jsfb/the-table.gjs similarity index 90% rename from smoke-tests/benchmark-app/app/templates/the-table.gjs rename to smoke-tests/benchmark-app/src/jsfb/the-table.gjs index 79c38146d68..4b2c8da84ea 100644 --- a/smoke-tests/benchmark-app/app/templates/the-table.gjs +++ b/smoke-tests/benchmark-app/src/jsfb/the-table.gjs @@ -1,13 +1,13 @@ import Component from '@glimmer/component'; import { fn } from '@ember/helper'; -import { service } from '@ember/service'; +import { State } from './state.js'; export class TheTable extends Component { - @service state; + state = new State(); /** * We currently don't have a way to emit HTML with no invisible characters - * and have the template look nice. + * and have the template look nice. */ -} \ No newline at end of file +} diff --git a/smoke-tests/benchmark-app/app/services/utils.js b/smoke-tests/benchmark-app/src/jsfb/utils.js similarity index 94% rename from smoke-tests/benchmark-app/app/services/utils.js rename to smoke-tests/benchmark-app/src/jsfb/utils.js index f6f3f1b5179..b5f2d06820e 100644 --- a/smoke-tests/benchmark-app/app/services/utils.js +++ b/smoke-tests/benchmark-app/src/jsfb/utils.js @@ -120,8 +120,3 @@ export const swapRows = (data) => { } }; -export const deleteRow = (data, id) => { - return data.filter((d) => { - return d.id !== id; - }); -}; \ No newline at end of file diff --git a/smoke-tests/benchmark-app/app/run-benchmark.js b/smoke-tests/benchmark-app/src/run-benchmark.js similarity index 51% rename from smoke-tests/benchmark-app/app/run-benchmark.js rename to smoke-tests/benchmark-app/src/run-benchmark.js index 1836c39c7bd..c51833bb247 100644 --- a/smoke-tests/benchmark-app/app/run-benchmark.js +++ b/smoke-tests/benchmark-app/src/run-benchmark.js @@ -1,6 +1,6 @@ const ButtonSelectors = { Create1000: '#run', - Create5000: '#runlots', + Create10000: '#runlots', Append1000: '#add', UpdateEvery10th: '#update', SelectFirstRow: 'tr:nth-child(1) a[data-test-select]', @@ -70,6 +70,67 @@ export function enforcePaintEvent() { } } +const TBODY_SELECTOR = 'table.test-data tbody'; + +function assert(condition, message) { + if (!condition) { + throw new Error(`Benchmark assertion failed: ${message}`); + } +} + +function rowCount() { + return document.querySelectorAll(`${TBODY_SELECTOR} tr`).length; +} + +// 1-based to mirror :nth-child semantics used elsewhere in this file. +function rowAt(oneBasedIndex) { + return document.querySelector(`${TBODY_SELECTOR} tr:nth-child(${oneBasedIndex})`); +} + +function rowIdAt(oneBasedIndex) { + const row = rowAt(oneBasedIndex); + if (!row) return null; + return row.querySelector('td.col-md-1').textContent; +} + +function rowLabelAt(oneBasedIndex) { + const row = rowAt(oneBasedIndex); + if (!row) return null; + return row.querySelector('a[data-test-select]').textContent; +} + +function rowIsSelected(oneBasedIndex) { + const row = rowAt(oneBasedIndex); + return row ? row.classList.contains('danger') : false; +} + +function selectedRowCount() { + return document.querySelectorAll(`${TBODY_SELECTOR} tr.danger`).length; +} + +function assertRowCount(expected, phase) { + const actual = rowCount(); + assert( + actual === expected, + `${phase}: expected ${expected} rows, got ${actual}` + ); +} + +function assertLabelSuffix(oneBasedIndex, suffix, phase) { + const label = rowLabelAt(oneBasedIndex); + assert( + label !== null && label.endsWith(suffix), + `${phase}: row ${oneBasedIndex} label ${JSON.stringify(label)} should end with ${JSON.stringify(suffix)}` + ); +} + +function assertNoLabelSuffix(oneBasedIndex, suffix, phase) { + const label = rowLabelAt(oneBasedIndex); + assert( + label !== null && !label.endsWith(suffix), + `${phase}: row ${oneBasedIndex} label ${JSON.stringify(label)} should NOT end with ${JSON.stringify(suffix)}` + ); +} /** * This probably isn't the best way to to tie in to the renderer, @@ -85,23 +146,24 @@ async function renderBenchmark() { resolveRender(); resolveRender = undefined; }); - + }); performance.measure('load', 'navigationStart', 'renderStart'); - return async (name, update) => { + return async (name, update, verify) => { console.log('measuring', name); await measureRender( name, name + 'Start', name + 'End', - () => + () => new Promise((resolve) => { update(); requestIdleCallback(resolve); }).catch(e => console.error(e)) ); + if (verify) verify(); }; } @@ -129,120 +191,206 @@ export async function runBenchmark() { await app('render1000Items1', () => { emitDomClickEvent(ButtonSelectors.Create1000); + }, () => { + assertRowCount(1000, 'render1000Items1'); }); await waitForIdle(); await app('clearItems1', () => { emitDomClickEvent(ButtonSelectors.Clear); + }, () => { + assertRowCount(0, 'clearItems1'); }); await waitForIdle(); await app('render1000Items2', () => { emitDomClickEvent(ButtonSelectors.Create1000); + }, () => { + assertRowCount(1000, 'render1000Items2'); }); await waitForIdle(); await app('clearItems2', () => { emitDomClickEvent(ButtonSelectors.Clear); + }, () => { + assertRowCount(0, 'clearItems2'); }); await waitForTraceFlush(); - await app('render5000Items1', () => { - emitDomClickEvent(ButtonSelectors.Create5000); + await app('render10000Items1', () => { + emitDomClickEvent(ButtonSelectors.Create10000); + }, () => { + assertRowCount(10000, 'render10000Items1'); }); await waitForTraceFlush(); await app('clearManyItems1', () => { emitDomClickEvent(ButtonSelectors.Clear); + }, () => { + assertRowCount(0, 'clearManyItems1'); }); await waitForTraceFlush(); - await app('render5000Items2', () => { - emitDomClickEvent(ButtonSelectors.Create5000); + await app('render10000Items2', () => { + emitDomClickEvent(ButtonSelectors.Create10000); + }, () => { + assertRowCount(10000, 'render10000Items2'); }); await waitForTraceFlush(); await app('clearManyItems2', () => { emitDomClickEvent(ButtonSelectors.Clear); + }, () => { + assertRowCount(0, 'clearManyItems2'); }); await waitForIdle(); await app('render1000Items3', () => { emitDomClickEvent(ButtonSelectors.Create1000); + }, () => { + assertRowCount(1000, 'render1000Items3'); }); await waitForIdle(); await app('append1000Items1', () => { emitDomClickEvent(ButtonSelectors.Append1000); + }, () => { + assertRowCount(2000, 'append1000Items1'); }); await waitForIdle(); await app('append1000Items2', () => { emitDomClickEvent(ButtonSelectors.Append1000); + }, () => { + assertRowCount(3000, 'append1000Items2'); }); await waitForIdle(); await app('updateEvery10thItem1', () => { emitDomClickEvent(ButtonSelectors.UpdateEvery10th); + }, () => { + // updates rows at array index 0, 10, 20, ... (DOM 1, 11, 21, ...) + assertRowCount(3000, 'updateEvery10thItem1'); + assertLabelSuffix(1, ' !!!', 'updateEvery10thItem1'); + assertLabelSuffix(11, ' !!!', 'updateEvery10thItem1'); + assertLabelSuffix(2991, ' !!!', 'updateEvery10thItem1'); + assertNoLabelSuffix(2, ' !!!', 'updateEvery10thItem1'); + assertNoLabelSuffix(10, ' !!!', 'updateEvery10thItem1'); }); await waitForIdle(); await app('updateEvery10thItem2', () => { emitDomClickEvent(ButtonSelectors.UpdateEvery10th); + }, () => { + assertRowCount(3000, 'updateEvery10thItem2'); + assertLabelSuffix(1, ' !!! !!!', 'updateEvery10thItem2'); + assertLabelSuffix(11, ' !!! !!!', 'updateEvery10thItem2'); + assertNoLabelSuffix(2, ' !!!', 'updateEvery10thItem2'); }); await waitForIdle(); await app('selectFirstRow1', () => { emitDomClickEvent(ButtonSelectors.SelectFirstRow); + }, () => { + assert(rowIsSelected(1), 'selectFirstRow1: row 1 should have class "danger"'); + assert( + selectedRowCount() === 1, + `selectFirstRow1: expected exactly 1 selected row, got ${selectedRowCount()}` + ); }); await waitForIdle(); await app('selectSecondRow1', () => { emitDomClickEvent(ButtonSelectors.SelectSecondRow); + }, () => { + assert(rowIsSelected(2), 'selectSecondRow1: row 2 should have class "danger"'); + assert(!rowIsSelected(1), 'selectSecondRow1: row 1 should no longer be selected'); + assert( + selectedRowCount() === 1, + `selectSecondRow1: expected exactly 1 selected row, got ${selectedRowCount()}` + ); }); await waitForIdle(); + // Capture id of row 1 so we can confirm it's actually gone after removal. + const preRemove1Id = rowIdAt(1); await app('removeFirstRow1', () => { emitDomClickEvent(ButtonSelectors.RemoveFirstRow); + }, () => { + assertRowCount(2999, 'removeFirstRow1'); + assert( + rowIdAt(1) !== preRemove1Id, + `removeFirstRow1: row at position 1 should no longer have id ${preRemove1Id}` + ); }); await waitForIdle(); + const preRemove2Id = rowIdAt(2); await app('removeSecondRow1', () => { emitDomClickEvent(ButtonSelectors.RemoveSecondRow); + }, () => { + assertRowCount(2998, 'removeSecondRow1'); + assert( + rowIdAt(2) !== preRemove2Id, + `removeSecondRow1: row at position 2 should no longer have id ${preRemove2Id}` + ); }); await waitForIdle(); + // state.swapRows swaps array indexes 1 and 998 → DOM positions 2 and 999. + const preSwapAt2 = rowIdAt(2); + const preSwapAt999 = rowIdAt(999); await app('swapRows1', () => { emitDomClickEvent(ButtonSelectors.SwapRows); + }, () => { + assert( + rowIdAt(2) === preSwapAt999, + `swapRows1: position 2 should now hold id ${preSwapAt999}, got ${rowIdAt(2)}` + ); + assert( + rowIdAt(999) === preSwapAt2, + `swapRows1: position 999 should now hold id ${preSwapAt2}, got ${rowIdAt(999)}` + ); }); await waitForIdle(); await app('swapRows2', () => { emitDomClickEvent(ButtonSelectors.SwapRows); + }, () => { + assert( + rowIdAt(2) === preSwapAt2, + `swapRows2: position 2 should be restored to ${preSwapAt2}, got ${rowIdAt(2)}` + ); + assert( + rowIdAt(999) === preSwapAt999, + `swapRows2: position 999 should be restored to ${preSwapAt999}, got ${rowIdAt(999)}` + ); }); await waitForIdle(); await app('clearItems4', () => { emitDomClickEvent(ButtonSelectors.Clear); + }, () => { + assertRowCount(0, 'clearItems4'); }); // finishing bench