diff --git a/.github/workflows/perf-check.yml b/.github/workflows/perf-check.yml index 6c839a96cfe..3b1b696828e 100644 --- a/.github/workflows/perf-check.yml +++ b/.github/workflows/perf-check.yml @@ -72,6 +72,16 @@ jobs: "experiment": "http://localhost:4201/basic-record-materialization", "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,end-record-materialization" }, + "complex-record-materialization": { + "control": "http://localhost:4200/complex-record-materialization", + "experiment": "http://localhost:4201/complex-record-materialization", + "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,end-record-materialization" + }, + "complex-record-materialization-with-relationship-materialization": { + "control": "http://localhost:4200/complex-record-materialization-with-relationship-materialization", + "experiment": "http://localhost:4201/complex-record-materialization-with-relationship-materialization", + "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,start-field-access,start-relationship-access,end-relationship-access" + }, "relationship-materialization-simple": { "control": "http://localhost:4200/relationship-materialization-simple", "experiment": "http://localhost:4201/relationship-materialization-simple", diff --git a/.github/workflows/perf-over-release.yml b/.github/workflows/perf-over-release.yml index 34114ded697..d85be697fef 100644 --- a/.github/workflows/perf-over-release.yml +++ b/.github/workflows/perf-over-release.yml @@ -71,6 +71,16 @@ jobs: "experiment": "http://localhost:4201/basic-record-materialization", "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,end-record-materialization" }, + "complex-record-materialization": { + "control": "http://localhost:4200/complex-record-materialization", + "experiment": "http://localhost:4201/complex-record-materialization", + "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,end-record-materialization" + }, + "complex-record-materialization-with-relationship-materialization": { + "control": "http://localhost:4200/complex-record-materialization-with-relationship-materialization", + "experiment": "http://localhost:4201/complex-record-materialization-with-relationship-materialization", + "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,start-field-access,start-relationship-access,end-relationship-access" + }, "relationship-materialization-simple": { "control": "http://localhost:4200/relationship-materialization-simple", "experiment": "http://localhost:4201/relationship-materialization-simple", diff --git a/tests/performance/app/mixins/README.md b/tests/performance/app/mixins/README.md new file mode 100644 index 00000000000..6a0ec491120 --- /dev/null +++ b/tests/performance/app/mixins/README.md @@ -0,0 +1,17 @@ +Relationships (mixins and model variants follow the same pattern): + +A hasOne B (1:1) +A hasOne C (1:many) +A hasOne D (1:none) + +B hasOne A (1:1) +B hasMany C (many:many) +B hasMany D (many:none) + +C hasMany A (many:1) +C hasMany B (many:many) +C hasMany D (many:none) + +D hasNone A +D hasNone B +D hasMany C (many:none) diff --git a/tests/performance/app/mixins/record-mixin-a.js b/tests/performance/app/mixins/record-mixin-a.js new file mode 100644 index 00000000000..2490c0f4320 --- /dev/null +++ b/tests/performance/app/mixins/record-mixin-a.js @@ -0,0 +1,35 @@ +// eslint-disable-next-line no-restricted-imports +import Mixin from '@ember/object/mixin'; + +import { attr, belongsTo } from '@ember-data/model'; + +export default Mixin.create({ + prop_trait_a_1: attr(), + prop_trait_a_2: attr(), + prop_trait_a_3: attr(), + prop_trait_a_4: attr(), + prop_trait_a_5: attr(), + prop_trait_a_6: attr(), + prop_trait_a_7: attr(), + prop_trait_a_8: attr(), + prop_trait_a_9: attr(), + prop_trait_a_10: attr(), + + belongsTo_trait_a_b: belongsTo('record-mixin-b', { + async: false, + inverse: 'belongsTo_trait_b_a', + polymorphic: true, + as: 'record-mixin-a', + }), + belongsTo_trait_a_c: belongsTo('record-mixin-c', { + async: false, + inverse: 'belongsTo_trait_c_a', + polymorphic: true, + as: 'record-mixin-a', + }), + belongsTo_trait_a_d: belongsTo('record-mixin-d', { + async: false, + inverse: null, + polymorphic: true, + }), +}); diff --git a/tests/performance/app/mixins/record-mixin-b.js b/tests/performance/app/mixins/record-mixin-b.js new file mode 100644 index 00000000000..6211845fd7b --- /dev/null +++ b/tests/performance/app/mixins/record-mixin-b.js @@ -0,0 +1,35 @@ +// eslint-disable-next-line no-restricted-imports +import Mixin from '@ember/object/mixin'; + +import { attr, belongsTo, hasMany } from '@ember-data/model'; + +export default Mixin.create({ + prop_trait_b_1: attr(), + prop_trait_b_2: attr(), + prop_trait_b_3: attr(), + prop_trait_b_4: attr(), + prop_trait_b_5: attr(), + prop_trait_b_6: attr(), + prop_trait_b_7: attr(), + prop_trait_b_8: attr(), + prop_trait_b_9: attr(), + prop_trait_b_10: attr(), + + belongsTo_trait_b_a: belongsTo('record-mixin-a', { + async: false, + inverse: 'belongsTo_trait_a_b', + polymorphic: true, + as: 'record-mixin-b', + }), + hasMany_trait_b_c: hasMany('record-mixin-c', { + async: false, + inverse: 'hasMany_trait_c_b', + polymorphic: true, + as: 'record-mixin-b', + }), + hasMany_trait_b_d: hasMany('record-mixin-d', { + async: false, + inverse: null, + polymorphic: true, + }), +}); diff --git a/tests/performance/app/mixins/record-mixin-c.js b/tests/performance/app/mixins/record-mixin-c.js new file mode 100644 index 00000000000..47a2d8f4ee8 --- /dev/null +++ b/tests/performance/app/mixins/record-mixin-c.js @@ -0,0 +1,35 @@ +// eslint-disable-next-line no-restricted-imports +import Mixin from '@ember/object/mixin'; + +import { attr, hasMany } from '@ember-data/model'; + +export default Mixin.create({ + prop_trait_c_1: attr(), + prop_trait_c_2: attr(), + prop_trait_c_3: attr(), + prop_trait_c_4: attr(), + prop_trait_c_5: attr(), + prop_trait_c_6: attr(), + prop_trait_c_7: attr(), + prop_trait_c_8: attr(), + prop_trait_c_9: attr(), + prop_trait_c_10: attr(), + + hasManyTo_trait_c_a: hasMany('record-mixin-a', { + async: false, + inverse: 'belongsTo_trait_a_c', + polymorphic: true, + as: 'record-mixin-c', + }), + hasMany_trait_c_b: hasMany('record-mixin-b', { + async: false, + inverse: 'hasMany_trait_b_c', + polymorphic: true, + as: 'record-mixin-c', + }), + hasMany_trait_c_d: hasMany('record-mixin-d', { + async: false, + inverse: null, + polymorphic: true, + }), +}); diff --git a/tests/performance/app/mixins/record-mixin-d.js b/tests/performance/app/mixins/record-mixin-d.js new file mode 100644 index 00000000000..d627c633bd8 --- /dev/null +++ b/tests/performance/app/mixins/record-mixin-d.js @@ -0,0 +1,23 @@ +// eslint-disable-next-line no-restricted-imports +import Mixin from '@ember/object/mixin'; + +import { attr, hasMany } from '@ember-data/model'; + +export default Mixin.create({ + prop_trait_d_1: attr(), + prop_trait_d_2: attr(), + prop_trait_d_3: attr(), + prop_trait_d_4: attr(), + prop_trait_d_5: attr(), + prop_trait_d_6: attr(), + prop_trait_d_7: attr(), + prop_trait_d_8: attr(), + prop_trait_d_9: attr(), + prop_trait_d_10: attr(), + + hasMany_trait_d_c: hasMany('record-mixin-c', { + async: false, + inverse: null, + polymorphic: true, + }), +}); diff --git a/tests/performance/app/mixins/record-mixin-e.js b/tests/performance/app/mixins/record-mixin-e.js new file mode 100644 index 00000000000..2b218d31ba9 --- /dev/null +++ b/tests/performance/app/mixins/record-mixin-e.js @@ -0,0 +1,8 @@ +// eslint-disable-next-line no-restricted-imports +import Mixin from '@ember/object/mixin'; + +import { attr } from '@ember-data/model'; + +export default Mixin.create({ + prop_trait_e_1: attr(), +}); diff --git a/tests/performance/app/mixins/record-mixin-f.js b/tests/performance/app/mixins/record-mixin-f.js new file mode 100644 index 00000000000..b4594e46cfc --- /dev/null +++ b/tests/performance/app/mixins/record-mixin-f.js @@ -0,0 +1,8 @@ +// eslint-disable-next-line no-restricted-imports +import Mixin from '@ember/object/mixin'; + +import { attr } from '@ember-data/model'; + +export default Mixin.create({ + prop_trait_f_1: attr(), +}); diff --git a/tests/performance/app/mixins/record-mixin-g.js b/tests/performance/app/mixins/record-mixin-g.js new file mode 100644 index 00000000000..63f2d4c1686 --- /dev/null +++ b/tests/performance/app/mixins/record-mixin-g.js @@ -0,0 +1,8 @@ +// eslint-disable-next-line no-restricted-imports +import Mixin from '@ember/object/mixin'; + +import { attr } from '@ember-data/model'; + +export default Mixin.create({ + prop_trait_g_1: attr(), +}); diff --git a/tests/performance/app/mixins/record-mixin-h.js b/tests/performance/app/mixins/record-mixin-h.js new file mode 100644 index 00000000000..8c55b0aa712 --- /dev/null +++ b/tests/performance/app/mixins/record-mixin-h.js @@ -0,0 +1,8 @@ +// eslint-disable-next-line no-restricted-imports +import Mixin from '@ember/object/mixin'; + +import { attr } from '@ember-data/model'; + +export default Mixin.create({ + prop_trait_h_1: attr(), +}); diff --git a/tests/performance/app/mixins/record-mixin-i.js b/tests/performance/app/mixins/record-mixin-i.js new file mode 100644 index 00000000000..b29ea298cc2 --- /dev/null +++ b/tests/performance/app/mixins/record-mixin-i.js @@ -0,0 +1,8 @@ +// eslint-disable-next-line no-restricted-imports +import Mixin from '@ember/object/mixin'; + +import { attr } from '@ember-data/model'; + +export default Mixin.create({ + prop_trait_i_1: attr(), +}); diff --git a/tests/performance/app/mixins/record-mixin-j.js b/tests/performance/app/mixins/record-mixin-j.js new file mode 100644 index 00000000000..c4918b2e3a7 --- /dev/null +++ b/tests/performance/app/mixins/record-mixin-j.js @@ -0,0 +1,8 @@ +// eslint-disable-next-line no-restricted-imports +import Mixin from '@ember/object/mixin'; + +import { attr } from '@ember-data/model'; + +export default Mixin.create({ + prop_trait_j_1: attr(), +}); diff --git a/tests/performance/app/models/complex-record-a.js b/tests/performance/app/models/complex-record-a.js new file mode 100644 index 00000000000..29d7c086c0e --- /dev/null +++ b/tests/performance/app/models/complex-record-a.js @@ -0,0 +1,43 @@ +import recordMixinA from 'app/mixins/record-mixin-a'; +import recordMixinB from 'app/mixins/record-mixin-b'; +import recordMixinC from 'app/mixins/record-mixin-c'; +import recordMixinD from 'app/mixins/record-mixin-d'; +import recordMixinE from 'app/mixins/record-mixin-e'; +import recordMixinF from 'app/mixins/record-mixin-f'; +import recordMixinG from 'app/mixins/record-mixin-g'; +import recordMixinH from 'app/mixins/record-mixin-h'; +import recordMixinI from 'app/mixins/record-mixin-i'; +import recordMixinJ from 'app/mixins/record-mixin-j'; + +import Model, { attr, belongsTo } from '@ember-data/model'; + +export default class ComplexRecordA extends Model.extend( + recordMixinA, + recordMixinB, + recordMixinC, + recordMixinD, + recordMixinE, + recordMixinF, + recordMixinG, + recordMixinH, + recordMixinI, + recordMixinJ +) { + @attr prop_resource_a_1; + @attr prop_resource_a_2; + @attr prop_resource_a_3; + @attr prop_resource_a_4; + @attr prop_resource_a_5; + @attr prop_resource_a_6; + @attr prop_resource_a_7; + @attr prop_resource_a_8; + @attr prop_resource_a_9; + @attr prop_resource_a_10; + + @belongsTo('complex-record-b', { async: false, inverse: 'belongsTo_resource_b_a' }) + belongsTo_resource_a_b; + @belongsTo('complex-record-c', { async: false, inverse: 'hasMany_resource_c_a' }) + belongsTo_resource_a_c; + @belongsTo('complex-record-d', { async: false, inverse: null }) + belongsTo_resource_a_d; +} diff --git a/tests/performance/app/models/complex-record-b.js b/tests/performance/app/models/complex-record-b.js new file mode 100644 index 00000000000..e1792885191 --- /dev/null +++ b/tests/performance/app/models/complex-record-b.js @@ -0,0 +1,43 @@ +import recordMixinA from 'app/mixins/record-mixin-a'; +import recordMixinB from 'app/mixins/record-mixin-b'; +import recordMixinC from 'app/mixins/record-mixin-c'; +import recordMixinD from 'app/mixins/record-mixin-d'; +import recordMixinE from 'app/mixins/record-mixin-e'; +import recordMixinF from 'app/mixins/record-mixin-f'; +import recordMixinG from 'app/mixins/record-mixin-g'; +import recordMixinH from 'app/mixins/record-mixin-h'; +import recordMixinI from 'app/mixins/record-mixin-i'; +import recordMixinJ from 'app/mixins/record-mixin-j'; + +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; + +export default class ComplexRecordB extends Model.extend( + recordMixinB, + recordMixinC, + recordMixinD, + recordMixinE, + recordMixinF, + recordMixinG, + recordMixinH, + recordMixinI, + recordMixinJ, + recordMixinA +) { + @attr prop_resource_b_1; + @attr prop_resource_b_2; + @attr prop_resource_b_3; + @attr prop_resource_b_4; + @attr prop_resource_b_5; + @attr prop_resource_b_6; + @attr prop_resource_b_7; + @attr prop_resource_b_8; + @attr prop_resource_b_9; + @attr prop_resource_b_10; + + @belongsTo('complex-record-a', { async: false, inverse: 'belongsTo_resource_a_b' }) + belongsTo_resource_b_a; + @hasMany('complex-record-c', { async: false, inverse: 'hasMany_resource_c_b' }) + hasMany_resource_b_c; + @hasMany('complex-record-d', { async: false, inverse: null }) + hasMany_resource_b_d; +} diff --git a/tests/performance/app/models/complex-record-c.js b/tests/performance/app/models/complex-record-c.js new file mode 100644 index 00000000000..3be8557ffd4 --- /dev/null +++ b/tests/performance/app/models/complex-record-c.js @@ -0,0 +1,43 @@ +import recordMixinA from 'app/mixins/record-mixin-a'; +import recordMixinB from 'app/mixins/record-mixin-b'; +import recordMixinC from 'app/mixins/record-mixin-c'; +import recordMixinD from 'app/mixins/record-mixin-d'; +import recordMixinE from 'app/mixins/record-mixin-e'; +import recordMixinF from 'app/mixins/record-mixin-f'; +import recordMixinG from 'app/mixins/record-mixin-g'; +import recordMixinH from 'app/mixins/record-mixin-h'; +import recordMixinI from 'app/mixins/record-mixin-i'; +import recordMixinJ from 'app/mixins/record-mixin-j'; + +import Model, { attr, hasMany } from '@ember-data/model'; + +export default class ComplexRecordC extends Model.extend( + recordMixinC, + recordMixinD, + recordMixinE, + recordMixinF, + recordMixinG, + recordMixinH, + recordMixinI, + recordMixinJ, + recordMixinA, + recordMixinB +) { + @attr prop_resource_c_1; + @attr prop_resource_c_2; + @attr prop_resource_c_3; + @attr prop_resource_c_4; + @attr prop_resource_c_5; + @attr prop_resource_c_6; + @attr prop_resource_c_7; + @attr prop_resource_c_8; + @attr prop_resource_c_9; + @attr prop_resource_c_10; + + @hasMany('complex-record-a', { async: false, inverse: 'belongsTo_resource_a_c' }) + hasMany_resource_c_a; + @hasMany('complex-record-b', { async: false, inverse: 'hasMany_resource_b_c' }) + hasMany_resource_c_b; + @hasMany('complex-record-d', { async: false, inverse: null }) + hasMany_resource_c_d; +} diff --git a/tests/performance/app/models/complex-record-d.js b/tests/performance/app/models/complex-record-d.js new file mode 100644 index 00000000000..af50ead20d4 --- /dev/null +++ b/tests/performance/app/models/complex-record-d.js @@ -0,0 +1,39 @@ +import recordMixinA from 'app/mixins/record-mixin-a'; +import recordMixinB from 'app/mixins/record-mixin-b'; +import recordMixinC from 'app/mixins/record-mixin-c'; +import recordMixinD from 'app/mixins/record-mixin-d'; +import recordMixinE from 'app/mixins/record-mixin-e'; +import recordMixinF from 'app/mixins/record-mixin-f'; +import recordMixinG from 'app/mixins/record-mixin-g'; +import recordMixinH from 'app/mixins/record-mixin-h'; +import recordMixinI from 'app/mixins/record-mixin-i'; +import recordMixinJ from 'app/mixins/record-mixin-j'; + +import Model, { attr, hasMany } from '@ember-data/model'; + +export default class ComplexRecordD extends Model.extend( + recordMixinD, + recordMixinE, + recordMixinF, + recordMixinG, + recordMixinH, + recordMixinI, + recordMixinJ, + recordMixinA, + recordMixinB, + recordMixinC +) { + @attr prop_resource_d_1; + @attr prop_resource_d_2; + @attr prop_resource_d_3; + @attr prop_resource_d_4; + @attr prop_resource_d_5; + @attr prop_resource_d_6; + @attr prop_resource_d_7; + @attr prop_resource_d_8; + @attr prop_resource_d_9; + @attr prop_resource_d_10; + + @hasMany('complex-record-c', { async: false, inverse: null }) + hasMany_resource_d_c; +} diff --git a/tests/performance/app/models/complex-record-e.js b/tests/performance/app/models/complex-record-e.js new file mode 100644 index 00000000000..97c13ab4c53 --- /dev/null +++ b/tests/performance/app/models/complex-record-e.js @@ -0,0 +1,36 @@ +import recordMixinA from 'app/mixins/record-mixin-a'; +import recordMixinB from 'app/mixins/record-mixin-b'; +import recordMixinC from 'app/mixins/record-mixin-c'; +import recordMixinD from 'app/mixins/record-mixin-d'; +import recordMixinE from 'app/mixins/record-mixin-e'; +import recordMixinF from 'app/mixins/record-mixin-f'; +import recordMixinG from 'app/mixins/record-mixin-g'; +import recordMixinH from 'app/mixins/record-mixin-h'; +import recordMixinI from 'app/mixins/record-mixin-i'; +import recordMixinJ from 'app/mixins/record-mixin-j'; + +import Model, { attr } from '@ember-data/model'; + +export default class ComplexRecordE extends Model.extend( + recordMixinE, + recordMixinF, + recordMixinG, + recordMixinH, + recordMixinI, + recordMixinJ, + recordMixinA, + recordMixinB, + recordMixinC, + recordMixinD +) { + @attr prop_resource_e_1; + @attr prop_resource_e_2; + @attr prop_resource_e_3; + @attr prop_resource_e_4; + @attr prop_resource_e_5; + @attr prop_resource_e_6; + @attr prop_resource_e_7; + @attr prop_resource_e_8; + @attr prop_resource_e_9; + @attr prop_resource_e_10; +} diff --git a/tests/performance/app/models/complex-record-f.js b/tests/performance/app/models/complex-record-f.js new file mode 100644 index 00000000000..ed1d25ac1b3 --- /dev/null +++ b/tests/performance/app/models/complex-record-f.js @@ -0,0 +1,36 @@ +import recordMixinA from 'app/mixins/record-mixin-a'; +import recordMixinB from 'app/mixins/record-mixin-b'; +import recordMixinC from 'app/mixins/record-mixin-c'; +import recordMixinD from 'app/mixins/record-mixin-d'; +import recordMixinE from 'app/mixins/record-mixin-e'; +import recordMixinF from 'app/mixins/record-mixin-f'; +import recordMixinG from 'app/mixins/record-mixin-g'; +import recordMixinH from 'app/mixins/record-mixin-h'; +import recordMixinI from 'app/mixins/record-mixin-i'; +import recordMixinJ from 'app/mixins/record-mixin-j'; + +import Model, { attr } from '@ember-data/model'; + +export default class ComplexRecordF extends Model.extend( + recordMixinF, + recordMixinG, + recordMixinH, + recordMixinI, + recordMixinJ, + recordMixinA, + recordMixinB, + recordMixinC, + recordMixinD, + recordMixinE +) { + @attr prop_resource_f_1; + @attr prop_resource_f_2; + @attr prop_resource_f_3; + @attr prop_resource_f_4; + @attr prop_resource_f_5; + @attr prop_resource_f_6; + @attr prop_resource_f_7; + @attr prop_resource_f_8; + @attr prop_resource_f_9; + @attr prop_resource_f_10; +} diff --git a/tests/performance/app/models/complex-record-g.js b/tests/performance/app/models/complex-record-g.js new file mode 100644 index 00000000000..b68f5a9465a --- /dev/null +++ b/tests/performance/app/models/complex-record-g.js @@ -0,0 +1,36 @@ +import recordMixinA from 'app/mixins/record-mixin-a'; +import recordMixinB from 'app/mixins/record-mixin-b'; +import recordMixinC from 'app/mixins/record-mixin-c'; +import recordMixinD from 'app/mixins/record-mixin-d'; +import recordMixinE from 'app/mixins/record-mixin-e'; +import recordMixinF from 'app/mixins/record-mixin-f'; +import recordMixinG from 'app/mixins/record-mixin-g'; +import recordMixinH from 'app/mixins/record-mixin-h'; +import recordMixinI from 'app/mixins/record-mixin-i'; +import recordMixinJ from 'app/mixins/record-mixin-j'; + +import Model, { attr } from '@ember-data/model'; + +export default class ComplexRecordG extends Model.extend( + recordMixinG, + recordMixinH, + recordMixinI, + recordMixinJ, + recordMixinA, + recordMixinB, + recordMixinC, + recordMixinD, + recordMixinE, + recordMixinF +) { + @attr prop_resource_g_1; + @attr prop_resource_g_2; + @attr prop_resource_g_3; + @attr prop_resource_g_4; + @attr prop_resource_g_5; + @attr prop_resource_g_6; + @attr prop_resource_g_7; + @attr prop_resource_g_8; + @attr prop_resource_g_9; + @attr prop_resource_g_10; +} diff --git a/tests/performance/app/models/complex-record-h.js b/tests/performance/app/models/complex-record-h.js new file mode 100644 index 00000000000..5906d23be69 --- /dev/null +++ b/tests/performance/app/models/complex-record-h.js @@ -0,0 +1,36 @@ +import recordMixinA from 'app/mixins/record-mixin-a'; +import recordMixinB from 'app/mixins/record-mixin-b'; +import recordMixinC from 'app/mixins/record-mixin-c'; +import recordMixinD from 'app/mixins/record-mixin-d'; +import recordMixinE from 'app/mixins/record-mixin-e'; +import recordMixinF from 'app/mixins/record-mixin-f'; +import recordMixinG from 'app/mixins/record-mixin-g'; +import recordMixinH from 'app/mixins/record-mixin-h'; +import recordMixinI from 'app/mixins/record-mixin-i'; +import recordMixinJ from 'app/mixins/record-mixin-j'; + +import Model, { attr } from '@ember-data/model'; + +export default class ComplexRecordH extends Model.extend( + recordMixinH, + recordMixinI, + recordMixinJ, + recordMixinA, + recordMixinB, + recordMixinC, + recordMixinD, + recordMixinE, + recordMixinF, + recordMixinG +) { + @attr prop_resource_h_1; + @attr prop_resource_h_2; + @attr prop_resource_h_3; + @attr prop_resource_h_4; + @attr prop_resource_h_5; + @attr prop_resource_h_6; + @attr prop_resource_h_7; + @attr prop_resource_h_8; + @attr prop_resource_h_9; + @attr prop_resource_h_10; +} diff --git a/tests/performance/app/models/complex-record-i.js b/tests/performance/app/models/complex-record-i.js new file mode 100644 index 00000000000..d269ab603d4 --- /dev/null +++ b/tests/performance/app/models/complex-record-i.js @@ -0,0 +1,36 @@ +import recordMixinA from 'app/mixins/record-mixin-a'; +import recordMixinB from 'app/mixins/record-mixin-b'; +import recordMixinC from 'app/mixins/record-mixin-c'; +import recordMixinD from 'app/mixins/record-mixin-d'; +import recordMixinE from 'app/mixins/record-mixin-e'; +import recordMixinF from 'app/mixins/record-mixin-f'; +import recordMixinG from 'app/mixins/record-mixin-g'; +import recordMixinH from 'app/mixins/record-mixin-h'; +import recordMixinI from 'app/mixins/record-mixin-i'; +import recordMixinJ from 'app/mixins/record-mixin-j'; + +import Model, { attr } from '@ember-data/model'; + +export default class ComplexRecordI extends Model.extend( + recordMixinI, + recordMixinJ, + recordMixinA, + recordMixinB, + recordMixinC, + recordMixinD, + recordMixinE, + recordMixinF, + recordMixinG, + recordMixinH +) { + @attr prop_resource_i_1; + @attr prop_resource_i_2; + @attr prop_resource_i_3; + @attr prop_resource_i_4; + @attr prop_resource_i_5; + @attr prop_resource_i_6; + @attr prop_resource_i_7; + @attr prop_resource_i_8; + @attr prop_resource_i_9; + @attr prop_resource_i_10; +} diff --git a/tests/performance/app/models/complex-record-j.js b/tests/performance/app/models/complex-record-j.js new file mode 100644 index 00000000000..1d2fb46070c --- /dev/null +++ b/tests/performance/app/models/complex-record-j.js @@ -0,0 +1,36 @@ +import recordMixinA from 'app/mixins/record-mixin-a'; +import recordMixinB from 'app/mixins/record-mixin-b'; +import recordMixinC from 'app/mixins/record-mixin-c'; +import recordMixinD from 'app/mixins/record-mixin-d'; +import recordMixinE from 'app/mixins/record-mixin-e'; +import recordMixinF from 'app/mixins/record-mixin-f'; +import recordMixinG from 'app/mixins/record-mixin-g'; +import recordMixinH from 'app/mixins/record-mixin-h'; +import recordMixinI from 'app/mixins/record-mixin-i'; +import recordMixinJ from 'app/mixins/record-mixin-j'; + +import Model, { attr } from '@ember-data/model'; + +export default class ComplexRecordJ extends Model.extend( + recordMixinJ, + recordMixinA, + recordMixinB, + recordMixinC, + recordMixinD, + recordMixinE, + recordMixinF, + recordMixinG, + recordMixinH, + recordMixinI +) { + @attr prop_resource_j_1; + @attr prop_resource_j_2; + @attr prop_resource_j_3; + @attr prop_resource_j_4; + @attr prop_resource_j_5; + @attr prop_resource_j_6; + @attr prop_resource_j_7; + @attr prop_resource_j_8; + @attr prop_resource_j_9; + @attr prop_resource_j_10; +} diff --git a/tests/performance/app/router.js b/tests/performance/app/router.js index 06ebd16dc9a..e5fadf0fe4c 100644 --- a/tests/performance/app/router.js +++ b/tests/performance/app/router.js @@ -9,6 +9,8 @@ const Router = EmberRouter.extend({ Router.map(function () { this.route('basic-record-materialization'); + this.route('complex-record-materialization'); + this.route('complex-record-materialization-with-relationship-materialization'); this.route('relationship-materialization-simple'); this.route('relationship-materialization-complex'); this.route('add-children'); diff --git a/tests/performance/app/routes/complex-record-materialization-with-relationship-materialization.js b/tests/performance/app/routes/complex-record-materialization-with-relationship-materialization.js new file mode 100644 index 00000000000..c0571325c33 --- /dev/null +++ b/tests/performance/app/routes/complex-record-materialization-with-relationship-materialization.js @@ -0,0 +1,66 @@ +import Route from '@ember/routing/route'; +import { service } from '@ember/service'; + +export default class extends Route { + @service store; + + async model() { + performance.mark('start-data-generation'); + const payload = await fetch('./fixtures/complex-record-materialization.json').then((r) => r.json()); + performance.mark('start-push-payload'); + this.store._push(payload); + performance.mark('start-peek-records'); + const complexRecordsA = this.store.peekAll('complex-record-a'); + const complexRecordsB = this.store.peekAll('complex-record-b'); + const complexRecordsC = this.store.peekAll('complex-record-c'); + const complexRecordsD = this.store.peekAll('complex-record-d'); + const complexRecordsE = this.store.peekAll('complex-record-e'); + const complexRecordsF = this.store.peekAll('complex-record-f'); + const complexRecordsG = this.store.peekAll('complex-record-g'); + const complexRecordsH = this.store.peekAll('complex-record-h'); + const complexRecordsI = this.store.peekAll('complex-record-i'); + const complexRecordsJ = this.store.peekAll('complex-record-j'); + performance.mark('start-record-materialization'); + const records = new Map([ + ['complex-record-a', complexRecordsA.slice()], + ['complex-record-b', complexRecordsB.slice()], + ['complex-record-c', complexRecordsC.slice()], + ['complex-record-d', complexRecordsD.slice()], + ['complex-record-e', complexRecordsE.slice()], + ['complex-record-f', complexRecordsF.slice()], + ['complex-record-g', complexRecordsG.slice()], + ['complex-record-h', complexRecordsH.slice()], + ['complex-record-i', complexRecordsI.slice()], + ['complex-record-j', complexRecordsJ.slice()], + ]); + performance.mark('start-field-access'); + + for (const [type, recordsOfType] of records) { + const fields = this.store.schema.fields({ type }); + for (const record of recordsOfType) { + for (const field of fields) { + if (field.kind === 'attribute') { + record[field]; + } + } + } + } + + performance.mark('start-relationship-access'); + + for (const [type, recordsOfType] of records) { + const fields = this.store.schema.fields({ type }); + for (const record of recordsOfType) { + for (const field of fields) { + if (field.kind === 'belongsTo') { + record[field]; + } else if (field.kind === 'hasMany') { + record[field].length; + } + } + } + } + + performance.mark('end-relationship-access'); + } +} diff --git a/tests/performance/app/routes/complex-record-materialization.js b/tests/performance/app/routes/complex-record-materialization.js new file mode 100644 index 00000000000..81c73bc50a6 --- /dev/null +++ b/tests/performance/app/routes/complex-record-materialization.js @@ -0,0 +1,36 @@ +import Route from '@ember/routing/route'; +import { service } from '@ember/service'; + +export default class extends Route { + @service store; + + async model() { + performance.mark('start-data-generation'); + const payload = await fetch('./fixtures/complex-record-materialization.json').then((r) => r.json()); + performance.mark('start-push-payload'); + this.store._push(payload); + performance.mark('start-peek-records'); + const complexRecordsA = this.store.peekAll('complex-record-a'); + const complexRecordsB = this.store.peekAll('complex-record-b'); + const complexRecordsC = this.store.peekAll('complex-record-c'); + const complexRecordsD = this.store.peekAll('complex-record-d'); + const complexRecordsE = this.store.peekAll('complex-record-e'); + const complexRecordsF = this.store.peekAll('complex-record-f'); + const complexRecordsG = this.store.peekAll('complex-record-g'); + const complexRecordsH = this.store.peekAll('complex-record-h'); + const complexRecordsI = this.store.peekAll('complex-record-i'); + const complexRecordsJ = this.store.peekAll('complex-record-j'); + performance.mark('start-record-materialization'); + complexRecordsA.slice(); + complexRecordsB.slice(); + complexRecordsC.slice(); + complexRecordsD.slice(); + complexRecordsE.slice(); + complexRecordsF.slice(); + complexRecordsG.slice(); + complexRecordsH.slice(); + complexRecordsI.slice(); + complexRecordsJ.slice(); + performance.mark('end-record-materialization'); + } +} diff --git a/tests/performance/fixtures/create-complex-payload.ts b/tests/performance/fixtures/create-complex-payload.ts new file mode 100644 index 00000000000..44f6efebdaa --- /dev/null +++ b/tests/performance/fixtures/create-complex-payload.ts @@ -0,0 +1,641 @@ +/* + There are 10 record types (a ... j). following the naming convention + `complex-record-{type}`. + + Each record type has 10 properties (1 ... 10) following the naming + convention `prop_resource_{type}_{number}`. + + Record types a, b, c, and d have relationships to each other. + + A hasOne B (1:1) + A hasOne C (1:many) + A hasOne D (1:none) + + B hasOne A (1:1) + B hasMany C (many:many) + B hasMany D (many:none) + + C hasMany A (many:1) + C hasMany B (many:many) + C hasMany D (many:none) + + D hasNone A + D hasNone B + D hasMany C (many:none) + + relationship names have the convention + + _resource__ + + Additionally, each record has 10 traits (mixins). following the naming + convention `record-mixin-{trait}` + + Each of the first 4 traits (a, b, c, d) has 10 properties (1 ... 10) following the naming + convention `prop_trait_{trait}_{number}`. The remaining traits have just one property still + following this convention. + + Traits a, b, c and d have relationships to each other in exactly the same + pattern as the record types; however, these relationships are set as + polymorphic. and thus any record type can satisfy them as all 10 record types + use all 10 traits. +*/ + +import { styleText } from 'node:util'; +import debug from 'debug'; + +const log = debug('create-complex-payload'); +const logRelationship = debug('create-complex-payload:relationships'); + +const types = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; +const fullTraits = ['a', 'b', 'c', 'd']; +const MANY_RELATIONSHIP_SIZE = 4; + +const SEEN_IDS_FOR = { + belongsTo_trait_a_b: new Set(), + belongsTo_trait_a_c: new Set(), + belongsTo_trait_a_d: new Set(), + belongsTo_trait_b_a: new Set(), + hasMany_trait_b_c: new Set(), + hasMany_trait_b_d: new Set(), + hasMany_trait_c_a: new Set(), + hasMany_trait_c_b: new Set(), + hasMany_trait_c_d: new Set(), + hasMany_trait_d_c: new Set(), + belongsTo_resource_a_b: new Set(), + belongsTo_resource_a_c: new Set(), + belongsTo_resource_a_d: new Set(), + belongsTo_resource_b_a: new Set(), + hasMany_resource_b_c: new Set(), + hasMany_resource_b_d: new Set(), + hasMany_resource_c_a: new Set(), + hasMany_resource_c_b: new Set(), + hasMany_resource_c_d: new Set(), + hasMany_resource_d_c: new Set(), +}; + +type RelationshipKey = keyof typeof SEEN_IDS_FOR; +type Ref = { type: string; id: string }; +type Resource = { + type: string; + id: string; + attributes: { [key: string]: string }; + relationships: { [key in RelationshipKey]?: { data: null | Ref[] | Ref } }; +}; +type Context = { + RecordsByRef: Map; + RecordsByType: Map; + ALL_Records: Resource[]; + totalPerType: number; + PrimaryRecords: Resource[]; + OtherRecords: Resource[]; +}; +type Meta = { + name: string; + kind: 'belongsTo' | 'hasMany'; + isRecord: boolean; + isTrait: boolean; + variant: string; + type: string; + inverse: string | null; +}; + +function generateRecordForType(type: string, id: string): Resource { + // each record will have 56 attributes + const attributes = {}; + for (let i = 1; i <= 10; i++) { + attributes[`prop_resource_${type}_${i}`] = `cr:${type}:${id}:${i}`; + } + for (const trait of types) { + for (let i = 1; i <= 10; i++) { + attributes[`prop_trait_${trait}_${i}`] = `trait_:${trait}:${id}:${i}`; + if (!fullTraits.includes(trait)) { + // exit the inner loop if we are not in the first 4 traits + // as only they have 10 properties, the rest have just 1 + break; + } + } + } + + /* + All records start with 10 relationships via traits a, b, c, and d. + The relationships are polymorphic and can be satisfied by any record type. + + Record types a, b, c, and d have additional relationships to each other. + + types a, b and c will have 13 total relationships + type d will have 11 total relationships. + + We start by populating these relationships as "empty". Then fill them + in during a second pass. + */ + const relationships: { [key in RelationshipKey]?: { data: null | Ref[] | Ref } } = { + belongsTo_trait_a_b: { data: null }, + belongsTo_trait_a_c: { data: null }, + belongsTo_trait_a_d: { data: null }, + belongsTo_trait_b_a: { data: null }, + hasMany_trait_b_c: { data: [] }, + hasMany_trait_b_d: { data: [] }, + hasMany_trait_c_a: { data: [] }, + hasMany_trait_c_b: { data: [] }, + hasMany_trait_c_d: { data: [] }, + hasMany_trait_d_c: { data: [] }, + }; + + if (type === 'a') { + relationships.belongsTo_resource_a_b = { data: null }; + relationships.belongsTo_resource_a_c = { data: null }; + relationships.belongsTo_resource_a_d = { data: null }; + } else if (type === 'b') { + relationships.belongsTo_resource_b_a = { data: null }; + relationships.hasMany_resource_b_c = { data: [] }; + relationships.hasMany_resource_b_d = { data: [] }; + } else if (type === 'c') { + relationships.hasMany_resource_c_a = { data: [] }; + relationships.hasMany_resource_c_b = { data: [] }; + relationships.hasMany_resource_c_d = { data: [] }; + } else if (type === 'd') { + relationships.hasMany_resource_d_c = { data: [] }; + } + + return { + id, + type: `complex-record-${type}`, + attributes, + relationships, + }; +} + +const REFS = new Map(); +function getRef(record: Resource): Ref { + if (REFS.has(record)) { + return REFS.get(record); + } + const _ref = { type: record.type, id: record.id }; + REFS.set(record, _ref); + return _ref; +} + +function printObjectRef(ref: Ref) { + return [ + styleText('gray', '{ '), + styleText('cyan', 'type'), + styleText('gray', ': '), + styleText('green', `"${ref.type}"`), + styleText('gray', ', '), + styleText('cyan', 'id'), + styleText('gray', ': '), + styleText('green', `"${ref.id}"`), + styleText('gray', ' }'), + ].join(''); +} + +function printStringRef(ref: Ref) { + return [ + styleText('gray', '{'), + styleText('cyan', ref.type), + styleText('gray', ':'), + styleText('green', ref.id), + styleText('gray', '}'), + ].join(''); +} + +function logArray(arr) { + console.log('\n\nArray<' + arr.length + '>['); + for (const item of arr) { + console.log(` ${printObjectRef(item)}`); + } + console.log(']\n'); +} + +function getNextUnseen(context: Context, selfRef: Ref, meta: Meta) { + const records = meta.isTrait ? context.ALL_Records : context.RecordsByType.get(meta.type); + + if (!records?.length) { + console.log({ + meta, + selfRef, + records, + }); + throw new Error('No records from which to find the next unseen'); + } + + const seen = SEEN_IDS_FOR[meta.name]; + + for (const record of records) { + const relatedRef = getRef(record); + if (seen.has(relatedRef)) { + continue; + } + if (relatedRef === selfRef) { + continue; + } + seen.add(relatedRef); + return record; + } + + if (meta.inverse === null) { + throw new Error(`No unseen records found for ${meta.kind} relationship ${meta.name} with inverse 'null'`); + } else if (meta.kind === 'belongsTo') { + throw new Error( + `No unseen records found for ${meta.kind} relationship ${meta.name} with inverse '${meta.inverse}'` + ); + } else { + console.log('\n\nseen', seen.size); + logArray(Array.from(seen)); + const inverseSeen = SEEN_IDS_FOR[meta.inverse]; + console.log('\n\ninverse seen', inverseSeen.size); + logArray(Array.from(inverseSeen)); + console.dir({ + meta, + selfRef, + inverseMeta: parseRelationshipMeta(meta.inverse), + }); + + throw new Error( + `No unseen records found for ${meta.kind} relationship ${meta.name} with inverse '${meta.inverse}'` + ); + } + + // for hasMany relationships, we are populating + + // // validate by printing out the state of every record's relationship + // console.log(`Validating States\n==================`); + // for (const record of all) { + // console.log(stringRef(getRef(record.type, record.id))); + // if (record.relationships[meta.name]) { + // const value = record.relationships[meta.name].data; + // const strValue = !value + // ? 'null' + // : Array.isArray(value) + // ? value.length + // ? value.map((v) => stringRef(v)).join(', ') + // : '' + // : stringRef(value); + // console.log(`\thas state ${meta.name}: ${strValue}`); + // } + // if (record.relationships[meta.inverse]) { + // const value = record.relationships[meta.inverse].data; + // const strValue = !value + // ? 'null' + // : Array.isArray(value) + // ? value.length + // ? value.map((v) => stringRef(v)).join(', ') + // : '' + // : stringRef(value); + // console.log(`\thas inverse ${meta.inverse}: ${strValue}`); + // } + // } + + // if (meta.kind === 'hasMany') { + // const passes = seen.passes ?? 0; + // if (passes < MANY_RELATIONSHIP_SIZE) { + // SEEN_IDS_FOR[meta.name].passes = passes + 1; + // SEEN_IDS_FOR[meta.name].clear(); + // } + + // return getNextUnseen(meta, selfRef, records, all); + // } + + // throw new Error('No unseen records found'); +} + +const INVERSE_RELATIONSHIPS = new Map([ + ['belongsTo_trait_a_b', 'belongsTo_trait_b_a'], + ['belongsTo_trait_a_c', 'hasMany_trait_c_a'], + ['belongsTo_trait_a_d', null], + ['belongsTo_trait_b_a', 'belongsTo_trait_a_b'], + ['hasMany_trait_b_c', 'hasMany_trait_c_b'], + ['hasMany_trait_b_d', null], + ['hasMany_trait_c_a', 'belongsTo_trait_a_c'], + ['hasMany_trait_c_b', 'hasMany_trait_b_c'], + ['hasMany_trait_c_d', null], + ['hasMany_trait_d_c', null], + + ['belongsTo_resource_a_b', 'belongsTo_resource_b_a'], + ['belongsTo_resource_a_c', 'hasMany_resource_c_a'], + ['belongsTo_resource_a_d', null], + ['belongsTo_resource_b_a', 'belongsTo_resource_a_b'], + ['hasMany_resource_b_c', 'hasMany_resource_c_b'], + ['hasMany_resource_b_d', null], + ['hasMany_resource_c_a', 'belongsTo_resource_a_c'], + ['hasMany_resource_c_b', 'hasMany_resource_b_c'], + ['hasMany_resource_c_d', null], + ['hasMany_resource_d_c', null], +]); + +/** + * the property keys follow the pattern + * - __ + * - __ + */ +const METAS = new Map(); +function parseRelationshipMeta(fieldName: string): Meta { + if (METAS.has(fieldName)) { + return METAS.get(fieldName); + } + const parts = fieldName.split('_'); + const kind = parts[0] as 'belongsTo' | 'hasMany'; + const isRecord = parts[1] === 'resource'; + const isTrait = parts[1] === 'trait'; + const ownType = parts[2]; + const typeVariant = parts[3]; + const relatedType = isTrait ? `record-mixin-${typeVariant}` : `complex-record-${typeVariant}`; + const inverse = INVERSE_RELATIONSHIPS.get(fieldName); + + if (inverse === undefined) { + throw new Error(`No inverse relationship found for ${fieldName}`); + } + + if (inverse && INVERSE_RELATIONSHIPS.get(inverse) !== fieldName) { + throw new Error(`Inverse relationship mismatch for ${fieldName}`); + } + + const meta = { + name: fieldName, + kind, + isRecord, + isTrait, + ownType, + variant: typeVariant, + type: relatedType, + inverse, + }; + + METAS.set(fieldName, meta); + + return meta; +} + +function canAddToRelatedArray(relatedArray: Ref[], ref: Ref) { + return ( + relatedArray.length < MANY_RELATIONSHIP_SIZE && + !relatedArray.some((rel) => rel.id === ref.id && ref.type === ref.type) + ); +} + +function addRelatedRecord(context: Context, record: Resource, meta: Meta) { + if (meta.kind !== 'belongsTo') { + throw new Error('only use addRelatedRecordForType for belongsTo relationships'); + } + + // if we've already been assigned, move on + const storage = record.relationships[meta.name]; + if (storage.data !== null) { + logRelationship(`\t\t${styleText('cyan', meta.name)} = ${printStringRef(storage.data)} [EXISTING]`); + return; + } + + const selfRef = getRef(record); + const candidate = getNextUnseen(context, selfRef, meta); + const ref = getRef(candidate); + + // in order to assign, we must also be able to assign to the inverse + if (!meta.inverse) { + logRelationship(`\t\t${styleText('cyan', meta.name)} = ${printStringRef(ref)}`); + storage.data = ref; + return; + } + const inverseMeta = parseRelationshipMeta(meta.inverse); + + if (inverseMeta.kind === 'hasMany') { + if (canAddToRelatedArray(candidate.relationships[inverseMeta.name].data, selfRef)) { + logRelationship(`\t\t${styleText('cyan', meta.name)} = ${printStringRef(ref)} (inverse updated)`); + storage.data = ref; + candidate.relationships[inverseMeta.name].data.push(selfRef); + SEEN_IDS_FOR[inverseMeta.name].add(selfRef); + return; + } else { + throw new Error(`Cannot add to ${inverseMeta.name} as it already is populated`); + } + } else if (inverseMeta.kind === 'belongsTo') { + if (candidate.relationships[inverseMeta.name].data === null) { + logRelationship(`\t\t${styleText('cyan', meta.name)} = ${printStringRef(ref)} (inverse updated)`); + storage.data = ref; + candidate.relationships[inverseMeta.name].data = selfRef; + SEEN_IDS_FOR[inverseMeta.name].add(selfRef); + return; + } else { + throw new Error(`Cannot add to ${inverseMeta.name} as it already is populated`); + } + } else { + throw new Error('Unknown inverse relationship kind'); + } +} + +function addRelatedRecords(context: Context, record: Resource, meta: Meta) { + if (meta.kind !== 'hasMany') { + throw new Error('only use addRelatedRecords for hasMany relationships'); + } + + // if we've already been assigned, move on + const storage = record.relationships[meta.name]; + if (storage.data.length >= MANY_RELATIONSHIP_SIZE) { + if (storage.data.length > MANY_RELATIONSHIP_SIZE) { + throw new Error('Too many relationships'); + } + logRelationship( + `\t\t${styleText('cyan', meta.name)} = [${storage.data.map(printStringRef).join(', ')}] [EXISTING]` + ); + return; + } + + const selfRef = getRef(record); + for (let i = storage.data.length; i < MANY_RELATIONSHIP_SIZE; i++) { + const candidate = getNextUnseen(context, selfRef, meta); + const ref = getRef(candidate); + + // in order to assign, we must also be able to assign to the inverse + if (!meta.inverse) { + logRelationship( + `\t\t${styleText('cyan', meta.name)} += ${printStringRef(ref)} (${storage.data.length + 1} total)` + ); + storage.data.push(ref); + continue; + } + const inverseMeta = parseRelationshipMeta(meta.inverse); + + if (inverseMeta.kind === 'hasMany') { + if (canAddToRelatedArray(candidate.relationships[inverseMeta.name].data, selfRef)) { + logRelationship( + `\t\t${styleText('cyan', meta.name)} += ${printStringRef(ref)} (${storage.data.length + 1} total) (inverse updated)` + ); + storage.data.push(ref); + candidate.relationships[inverseMeta.name].data.push(selfRef); + SEEN_IDS_FOR[inverseMeta.name].add(selfRef); + // IF NEEDED + // in a Many:Many situation, we can reset SEEN_IDS in between as the only requirement + // on uniqueness is that its not present in the candidate + continue; + } else { + throw new Error(`Cannot add to ${inverseMeta.name} as it already is populated`); + } + } else if (inverseMeta.kind === 'belongsTo') { + if (candidate.relationships[inverseMeta.name].data === null) { + logRelationship( + `\t\t${styleText('cyan', meta.name)} += ${printStringRef(ref)} (${storage.data.length + 1} total) (inverse updated)` + ); + storage.data.push(ref); + candidate.relationships[inverseMeta.name].data = selfRef; + SEEN_IDS_FOR[inverseMeta.name].add(selfRef); + continue; + } else { + throw new Error( + `Cannot add ${printStringRef(selfRef)} to ${printStringRef(ref)} field ${inverseMeta.name} as it already is populated with ${printStringRef(candidate.relationships[inverseMeta.name].data)}` + ); + } + } else { + throw new Error('Unknown inverse relationship kind'); + } + } +} + +async function ensureRelationshipData(context: Context) { + log( + `Generated ${context.ALL_Records.length} total records. Ensuring complete relationship data for ${context.totalPerType} records per type` + ); + // for hasMany relationships, we always want to have MANY_RELATIONSHIP_SIZE records. + // for belongsTo relationships, we want to have 1 record. + // for mixin relationships, any record type satisfies the relationship + // for non-mixin relationships, only the specific record type satisfies the relationship + + // we process all belongsTo relationships first so that they are most likely to point + // as other primary records + for (const recordsForType of context.RecordsByType.values()) { + let processed = 0; + for (const record of recordsForType) { + logRelationship(`BelongsTo | ${printStringRef(record)} (${++processed} of ${context.totalPerType})`); + + for (const fieldName of Object.keys(record.relationships)) { + const meta = parseRelationshipMeta(fieldName); + if (meta.kind === 'belongsTo') { + addRelatedRecord(context, record, meta); + } else if (meta.kind === 'hasMany') { + // do nothing, handled below + } else { + throw new Error('Unknown relationship kind'); + } + } + + // only add relationships to the first totalPerType records + if (processed >= context.totalPerType) { + break; + } + + logRelationship(`\n\n`); + if (logRelationship.enabled) { + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } + + logRelationship(`\n\n====================\n\n`); + if (logRelationship.enabled) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } + + // next we process hasMany relationships + for (const recordsForType of context.RecordsByType.values()) { + let processed = 0; + for (const record of recordsForType) { + logRelationship(`HasMany | ${printStringRef(record)} (${++processed} of ${context.totalPerType})`); + + for (const fieldName of Object.keys(record.relationships)) { + const meta = parseRelationshipMeta(fieldName); + if (meta.kind === 'belongsTo') { + // do nothing, handled above + } else if (meta.kind === 'hasMany') { + addRelatedRecords(context, record, meta); + } else { + throw new Error('Unknown relationship kind'); + } + } + + // only add relationships to the first totalPerType records + if (processed >= context.totalPerType) { + break; + } + + logRelationship(`\n\n`); + if (logRelationship.enabled) { + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } + + logRelationship(`\n\n====================\n\n`); + if (logRelationship.enabled) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } + + // finally we validate + for (const recordsForType of context.RecordsByType.values()) { + let processed = 0; + for (const record of recordsForType) { + for (const fieldName of Object.keys(record.relationships)) { + const meta = parseRelationshipMeta(fieldName); + if (meta.kind === 'belongsTo') { + if (record.relationships[meta.name].data === null) { + console.log(`\tMissing belongsTo relationship ${meta.name} on ${record.type} ${record.id}`); + } + } else if (meta.kind === 'hasMany') { + if (record.relationships[meta.name].data.length !== MANY_RELATIONSHIP_SIZE) { + console.log( + `\tInvalid hasMany relationship ${meta.name} on ${record.type} ${record.id}, expected ${MANY_RELATIONSHIP_SIZE} got ${record.relationships[meta.name].data.length}` + ); + } + } else { + throw new Error('Unknown relationship kind'); + } + } + processed++; + if (processed >= context.totalPerType) { + break; + } + } + } +} + +async function createComplexPayload(totalPerType = 100) { + const RecordsByType = new Map(); + const RecordsByRef = new Map(); + const ALL_Records: Resource[] = []; + const PrimaryRecords: Resource[] = []; + const OtherRecords: Resource[] = []; + + // generate the records + for (const type of types) { + const RecordsOfType: Resource[] = []; + RecordsByType.set(`complex-record-${type}`, RecordsOfType); + + // in order to have 1:many and many:many relationships of N > 1, we need + // to generate N * desired total records. + for (let i = 1; i <= totalPerType * (MANY_RELATIONSHIP_SIZE + 1); i++) { + const id = String(i); + const record = generateRecordForType(type, id); + const ref = getRef(record); + RecordsByRef.set(ref, record); + RecordsOfType.push(record); + ALL_Records.push(record); + + if (i <= totalPerType) { + PrimaryRecords.push(record); + } else { + OtherRecords.push(record); + } + } + } + + // fill in the relationships + await ensureRelationshipData({ + RecordsByRef, + RecordsByType, + ALL_Records, + totalPerType, + PrimaryRecords, + OtherRecords, + }); + + return { data: PrimaryRecords, included: OtherRecords }; +} + +export { createComplexPayload }; diff --git a/tests/performance/fixtures/generated/complex-record-materialization.json.br b/tests/performance/fixtures/generated/complex-record-materialization.json.br new file mode 100644 index 00000000000..ec76a16a073 Binary files /dev/null and b/tests/performance/fixtures/generated/complex-record-materialization.json.br differ diff --git a/tests/performance/fixtures/index.js b/tests/performance/fixtures/index.js index ee190736ec2..98e1bd34005 100644 --- a/tests/performance/fixtures/index.js +++ b/tests/performance/fixtures/index.js @@ -16,7 +16,7 @@ function compress(code) { function write(name, json) { console.log( `\tGenerated fixtures for ${name}: ${Array.isArray(json.data) ? json.data.length : 1} primary, ${ - json.included.length + json.included?.length ?? 0 } included\n` ); fs.writeFileSync(`./fixtures/generated/${name}.json.br`, compress(JSON.stringify(json))); @@ -25,15 +25,20 @@ function write(name, json) { const createParentPayload = require('./create-parent-payload'); const createCarsPayload = require('./create-cars-payload'); const createParentRecords = require('./create-parent-records'); +const { createComplexPayload: createComplexRecordsPayload } = require('./create-complex-payload.ts'); -write('add-children-initial', createParentPayload(19600)); -write('add-children-final', createParentPayload(20000)); -write('destroy', createParentPayload(500, 50)); -write('relationship-materialization-simple', createCarsPayload(10000)); -write('relationship-materialization-complex', createParentRecords(200, 10, 20)); -write('unload', createParentPayload(500, 50)); -write('unload-all', createParentRecords(1000, 5, 10)); -write('unused-relationships', createParentPayload(500, 50)); -write('example-car', createCarsPayload(1)); -write('example-parent', createParentPayload(2, 2)); -write('basic-record-materialization', createParentRecords(10000, 2, 3)); +async function main() { + write('add-children-initial', createParentPayload(19600)); + write('add-children-final', createParentPayload(20000)); + write('destroy', createParentPayload(500, 50)); + write('relationship-materialization-simple', createCarsPayload(10000)); + write('relationship-materialization-complex', createParentRecords(200, 10, 20)); + write('unload', createParentPayload(500, 50)); + write('unload-all', createParentRecords(1000, 5, 10)); + write('unused-relationships', createParentPayload(500, 50)); + write('example-car', createCarsPayload(1)); + write('example-parent', createParentPayload(2, 2)); + write('basic-record-materialization', createParentRecords(10000, 2, 3)); + write('complex-record-materialization', await createComplexRecordsPayload(5)); +} +main();