From 268283431da020f694703786e1f3c9b9e2ee6a81 Mon Sep 17 00:00:00 2001 From: Francois G Date: Tue, 19 Jul 2022 04:28:38 -0400 Subject: [PATCH] Improvements to the test perfs API (#54) --- src/testingPerfs/data/data.resolvers.ts | 211 ++++++++++++++++------- src/utils/testing/types/perf.ts | 50 +++++- src/utils/testing/types/perfAverage.ts | 6 +- src/utils/testing/types/perfRun.ts | 5 +- src/utils/testing/types/statistics.ts | 55 +++--- src/utils/testing/types/tag.ts | 15 ++ src/utils/testing/types/tagConnection.ts | 18 ++ src/utils/testing/types/tagEdge.ts | 12 ++ 8 files changed, 265 insertions(+), 107 deletions(-) create mode 100644 src/utils/testing/types/tag.ts create mode 100644 src/utils/testing/types/tagConnection.ts create mode 100644 src/utils/testing/types/tagEdge.ts diff --git a/src/testingPerfs/data/data.resolvers.ts b/src/testingPerfs/data/data.resolvers.ts index 0c282bd..e4466b4 100644 --- a/src/testingPerfs/data/data.resolvers.ts +++ b/src/testingPerfs/data/data.resolvers.ts @@ -43,12 +43,20 @@ export default class DataPerfResolver { }) size: number, @Args({ - name: 'transaction', + name: 'transactions', + type: () => [String], + description: 'Array of transactions keys to return, * for all', + nullable: true, + defaultValue: ['Total'], + }) + transactions: string, + @Args({ + name: 'profileName', type: () => String, - description: 'Transaction to return within runs', + description: 'Name of the profile to return, leave empty for all', nullable: true, }) - transaction: string, + profileName: string, @Args({ name: 'orderBy', type: () => ItemSortorder, @@ -62,20 +70,19 @@ export default class DataPerfResolver { nullable: true, defaultValue: false, }) - includeDisabled: boolean, + includeDisabled: boolean, @Parent() parent: Data, ) { const userConfig = this.confService.getUserConfig(); - const filterTransaction = transaction === null ? "Total" : transaction const data = await this.itemsService.findAll( first, size, parent.query, orderBy, userConfig.elasticsearch.dataIndices.testingPerfs + '*', - includeDisabled + includeDisabled, ); // This should probably be done way better since here we're fetching everything from Elasticsearch @@ -84,17 +91,31 @@ export default class DataPerfResolver { ...d, runs: { ...d.runs, - edges: d.runs.edges.map((r) => { - return { - node: { - ...r.node, - statistics: r.node.statistics.filter((s) => s.transaction === filterTransaction) + edges: d.runs.edges + .filter((r) => { + if (!profileName) { + return true; } - } - }) - } - } - }) + return r.node.name === profileName; + }) + .map((r) => { + return { + node: { + ...r.node, + statistics: r.node.statistics + .map((o) => (o.transaction === undefined ? Object.values(o)[0] : o)) + .filter((s) => { + if (transactions[0] === '*') { + return true; + } + return transactions.includes(s.transaction); + }), + }, + }; + }), + }, + }; + }); return { ...data, @@ -121,7 +142,7 @@ export default class DataPerfResolver { description: 'Return a single profile (a single run within a run)', nullable: true, }) - profileId: string, + profileId: string, // eslint-disable-next-line @typescript-eslint/no-unused-vars @Parent() parent: Data, ): Promise { @@ -131,23 +152,35 @@ export default class DataPerfResolver { } const item = await this.itemsService.findOneById(id, userConfig.elasticsearch.dataIndices.testingPerfs); if (profileId !== undefined && profileId !== null) { - const selectedRun = item.runs.edges.find((r) => r.node.id === profileId) + const selectedRun = item.runs.edges.find((r) => r.node.id === profileId); if (selectedRun !== undefined) { const updatedItem = { ...item, run: { ...selectedRun.node, id: `${id} - ${selectedRun.node.id}`, - } - } - delete item.runs - return updatedItem + }, + }; + delete item.runs; + return updatedItem; } } return { ...item, - _source: JSON.stringify(item) + // Some older dataset might be using a different model, this converts it to a common format + runs: { + ...item.runs, + edges: item.runs.edges.map((r: any) => { + return { + node: { + ...r.node, + statistics: r.node.statistics.map((o) => (o.transaction === undefined ? Object.values(o)[0] : o)), + }, + }; + }), + }, + _source: JSON.stringify(item), }; } @@ -176,7 +209,7 @@ export default class DataPerfResolver { description: 'Additional options as a stringified object (more details in the documentation)', nullable: true, }) - options: string, + options: string, @Parent() parent: Data, ) { @@ -211,15 +244,27 @@ export default class DataPerfResolver { description: 'Query to fetch documents to be used to calculate the averages', nullable: true, }) - averageQuery: string, + averageQuery: string, @Args({ name: 'statsKeys', type: () => [String], description: 'Array of statistics keys to return', nullable: true, - defaultValue: ['sampleCount', 'errorCount', 'errorPct', 'meanResTime', 'medianResTime', 'minResTime', 'maxResTime', 'pct1ResTime', 'pct2ResTime', 'pct3ResTime', 'throughput'] + defaultValue: [ + 'sampleCount', + 'errorCount', + 'errorPct', + 'meanResTime', + 'medianResTime', + 'minResTime', + 'maxResTime', + 'pct1ResTime', + 'pct2ResTime', + 'pct3ResTime', + 'throughput', + ], }) - statsKeys: string, + statsKeys: string, @Parent() parent: Data, ) { @@ -230,13 +275,13 @@ export default class DataPerfResolver { } // Get all records from the parent query, the records fetched using the other query MUST be part of that first array. - // This is to make sure that if we run an average on on a particular image, + // This is to make sure that if we run an average on on a particular image, // we don't collect records corresponding to another type of run that might happen to be using the same image const dataParent = await this.itemsService.findAll( 0, 10000, parent.query, - {direction: 'DESC', field: 'startedAt'}, + { direction: 'DESC', field: 'startedAt' }, userConfig.elasticsearch.dataIndices.testingPerfs + '*', ); @@ -244,17 +289,17 @@ export default class DataPerfResolver { 0, 10000, averageQuery, - {direction: 'DESC', field: 'startedAt'}, + { direction: 'DESC', field: 'startedAt' }, userConfig.elasticsearch.dataIndices.testingPerfs + '*', ); - + // Place the data in an array filtered on the profile ID - const filteredData: any = [] + const filteredData: any = []; // Filtering out any document that might not be in the parent query. - for (const item of data.nodes.filter((n) => dataParent.nodes.find((p) => p.id === n.id) !== undefined )) { - const selectedRun = item.runs.edges.filter((r) => r.node !== undefined).find((r) => r.node.name === profileId) + for (const item of data.nodes.filter((n) => dataParent.nodes.find((p) => p.id === n.id) !== undefined)) { + const selectedRun = item.runs.edges.filter((r) => r.node !== undefined).find((r) => r.node.name === profileId); if (selectedRun !== undefined) { - filteredData.push({...item, statistics: selectedRun.node.statistics}) + filteredData.push({ ...item, statistics: selectedRun.node.statistics }); } } @@ -262,43 +307,43 @@ export default class DataPerfResolver { return null; } - const transactions = filteredData[0].statistics.map((t) => t.transaction) + const transactions = filteredData[0].statistics.map((t) => t.transaction); - const statsComputed = [] + const statsComputed = []; // Iterate over all available transactions for (const t of transactions) { for (const key of statsKeys) { - const values = [] + const values = []; for (const run of filteredData) { - const currentValue = run.statistics.find((st) => t === st.transaction) + const currentValue = run.statistics.find((st) => t === st.transaction); if (currentValue !== undefined) { - values.push({value: currentValue[key], run: {id: run.id, name: run.name, startedAt: run.startedAt}}) + values.push({ value: currentValue[key], run: { id: run.id, name: run.name, startedAt: run.startedAt } }); } } statsComputed.push({ statisticsKey: key, transaction: t, - value: values.map(v => v.value).reduce((acc, v) => acc + v, 0) / values.length, - runs: values.map(v => { + value: values.map((v) => v.value).reduce((acc, v) => acc + v, 0) / values.length, + runs: values.map((v) => { return { id: `${v.run.id}-${t}-${key}`, name: v.run.name, startedAt: v.run.startedAt, - value: v.value - } - }) - }) + value: v.value, + }; + }), + }); } } return { - id: 'average', + id: 'average', runs: filteredData, statisticsKeys: statsKeys, transactions: transactions, average: statsComputed, // values: statsValues - } + }; } @Mutation(() => Perf, { @@ -306,8 +351,8 @@ export default class DataPerfResolver { description: 'Prevent a testing perfs run from being included in the results', }) async disableTestingsPerfsRuns( - @Args({ name: 'id', type: () => String }) id: string, - @Args({ name: 'username', type: () => String, nullable: true, defaultValue: ''}) username: string + @Args({ name: 'id', type: () => String }) id: string, + @Args({ name: 'username', type: () => String, nullable: true, defaultValue: '' }) username: string, ) { const userConfig = this.confService.getUserConfig(); if (id === '') { @@ -324,8 +369,8 @@ export default class DataPerfResolver { description: 'Prevent a testing perfs run from being included in the results', }) async enableTestingsPerfsRuns( - @Args({ name: 'id', type: () => String }) id: string, - @Args({ name: 'username', type: () => String, nullable: true, defaultValue: ''}) username: string + @Args({ name: 'id', type: () => String }) id: string, + @Args({ name: 'username', type: () => String, nullable: true, defaultValue: '' }) username: string, ) { const userConfig = this.confService.getUserConfig(); if (id === '') { @@ -342,14 +387,20 @@ export default class DataPerfResolver { description: 'Mark a testing perf run as unverified', }) async unverifyTestingsPerfsRuns( - @Args({ name: 'id', type: () => String }) id: string, - @Args({ name: 'username', type: () => String, nullable: true, defaultValue: ''}) username: string + @Args({ name: 'id', type: () => String }) id: string, + @Args({ name: 'username', type: () => String, nullable: true, defaultValue: '' }) username: string, ) { const userConfig = this.confService.getUserConfig(); if (id === '') { return null; } - await this.itemsService.updateDocumentField(id, userConfig.elasticsearch.dataIndices.testingPerfs, username, false, 'verified'); + await this.itemsService.updateDocumentField( + id, + userConfig.elasticsearch.dataIndices.testingPerfs, + username, + false, + 'verified', + ); const item = await this.itemsService.findOneById(id, userConfig.elasticsearch.dataIndices.testingPerfs); return item; } @@ -359,15 +410,21 @@ export default class DataPerfResolver { description: 'Mark a testing perf run as verified', }) async verifyTestingsPerfsRuns( - @Args({ name: 'id', type: () => String }) id: string, - @Args({ name: 'username', type: () => String, nullable: true, defaultValue: ''}) username: string + @Args({ name: 'id', type: () => String }) id: string, + @Args({ name: 'username', type: () => String, nullable: true, defaultValue: '' }) username: string, ) { const userConfig = this.confService.getUserConfig(); if (id === '') { return null; } - await this.itemsService.updateDocumentField(id, userConfig.elasticsearch.dataIndices.testingPerfs, username, true, 'verified'); + await this.itemsService.updateDocumentField( + id, + userConfig.elasticsearch.dataIndices.testingPerfs, + username, + true, + 'verified', + ); const item = await this.itemsService.findOneById(id, userConfig.elasticsearch.dataIndices.testingPerfs); return item; } @@ -377,10 +434,11 @@ export default class DataPerfResolver { description: 'Update description and analysis for a run', }) async updateTestingsPerfsRun( - @Args({ name: 'id', type: () => String }) id: string, - @Args({ name: 'username', type: () => String, nullable: true, defaultValue: ''}) username: string, - @Args({ name: 'description', type: () => String, nullable: true, defaultValue: null}) description: string, - @Args({ name: 'analysis', type: () => String, nullable: true, defaultValue: null}) analysis: string + @Args({ name: 'id', type: () => String }) id: string, + @Args({ name: 'username', type: () => String, nullable: true, defaultValue: '' }) username: string, + @Args({ name: 'group', type: () => String, nullable: true, defaultValue: '' }) group: string, + @Args({ name: 'description', type: () => String, nullable: true, defaultValue: null }) description: string, + @Args({ name: 'analysis', type: () => String, nullable: true, defaultValue: null }) analysis: string, ) { const userConfig = this.confService.getUserConfig(); @@ -389,12 +447,33 @@ export default class DataPerfResolver { } if (description !== null) { - await this.itemsService.updateDocumentField(id, userConfig.elasticsearch.dataIndices.testingPerfs, username, description, 'description'); + await this.itemsService.updateDocumentField( + id, + userConfig.elasticsearch.dataIndices.testingPerfs, + username, + description, + 'description', + ); } if (analysis !== null) { - await this.itemsService.updateDocumentField(id, userConfig.elasticsearch.dataIndices.testingPerfs, username, analysis, 'analysis'); - } + await this.itemsService.updateDocumentField( + id, + userConfig.elasticsearch.dataIndices.testingPerfs, + username, + analysis, + 'analysis', + ); + } + if (group !== null) { + await this.itemsService.updateDocumentField( + id, + userConfig.elasticsearch.dataIndices.testingPerfs, + username, + group, + 'group', + ); + } const item = await this.itemsService.findOneById(id, userConfig.elasticsearch.dataIndices.testingPerfs); return item; - } + } } diff --git a/src/utils/testing/types/perf.ts b/src/utils/testing/types/perf.ts index ddf5c3f..9bb1f53 100644 --- a/src/utils/testing/types/perf.ts +++ b/src/utils/testing/types/perf.ts @@ -1,10 +1,11 @@ import { Field, ObjectType, ID } from '@nestjs/graphql'; import ResourceConnection from './resourceConnection'; +import TagConnection from './tagConnection'; import PerfRunConnection from './perfRunConnection'; import PerfRun from './perfRun'; import Platform from './platform'; -import RepositoryConnection from '../../github/types/repositoryConnection' +import RepositoryConnection from '../../github/types/repositoryConnection'; @ObjectType() export default class Perf { @@ -31,6 +32,18 @@ export default class Perf { }) description: string; + @Field(() => String, { + nullable: true, + description: 'Who did the description', + }) + description_by: string; + + @Field(() => String, { + nullable: true, + description: 'Date the descruption was added', + }) + description_date: string; + @Field(() => String, { nullable: true, description: 'Analysis of the run, written by a team member', @@ -41,7 +54,7 @@ export default class Perf { nullable: true, description: 'Who did the analysis', }) - analysis_by: string; + analysis_by: string; @Field(() => String, { nullable: true, @@ -49,6 +62,24 @@ export default class Perf { }) analysis_date: string; + @Field(() => String, { + nullable: true, + description: 'Group set by a user to allow quick filtering', + }) + group: string; + + @Field(() => String, { + nullable: true, + description: 'Who did set the group', + }) + group_by: string; + + @Field(() => String, { + nullable: true, + description: 'When was the group set', + }) + group_date: string; + @Field(() => Boolean, { nullable: true, description: 'Was the run verified and relevant ?', @@ -59,7 +90,7 @@ export default class Perf { nullable: true, description: 'Who verified therun', }) - verified_by: string; + verified_by: string; @Field(() => String, { nullable: true, @@ -95,7 +126,7 @@ export default class Perf { nullable: true, description: 'Rampup used for the run', }) - rampUp: number; + rampUp: number; @Field(() => Platform, { nullable: false, @@ -109,6 +140,12 @@ export default class Perf { }) resources: ResourceConnection; + @Field(() => TagConnection, { + nullable: false, + description: 'List of tags associated with the run', + }) + tags: TagConnection; + @Field(() => PerfRunConnection, { nullable: true, description: 'Runs executed in the tests', @@ -119,7 +156,7 @@ export default class Perf { nullable: false, description: 'Run corresponding to the selected profile (provided using profileid)', }) - run: PerfRun; + run: PerfRun; @Field(() => String, { nullable: true, @@ -143,6 +180,5 @@ export default class Perf { nullable: true, description: 'When was the run disabled', }) - disabled_date: string; - + disabled_date: string; } diff --git a/src/utils/testing/types/perfAverage.ts b/src/utils/testing/types/perfAverage.ts index 0dbe857..733f487 100644 --- a/src/utils/testing/types/perfAverage.ts +++ b/src/utils/testing/types/perfAverage.ts @@ -30,14 +30,14 @@ export default class PerfAverage { values: PerfAverageValue[]; @Field(() => [String], { - nullable: false, + nullable: true, description: 'List of available transactions', }) - transactions: [string]; + transactions: [string]; @Field(() => [String], { nullable: false, description: 'List of available statistics keys', }) - statisticsKeys: [string]; + statisticsKeys: [string]; } diff --git a/src/utils/testing/types/perfRun.ts b/src/utils/testing/types/perfRun.ts index 97b4426..00f6f9d 100644 --- a/src/utils/testing/types/perfRun.ts +++ b/src/utils/testing/types/perfRun.ts @@ -1,6 +1,6 @@ import { Field, ObjectType, ID } from '@nestjs/graphql'; -import Statistics from './statistics' +import Statistics from './statistics'; @ObjectType() export default class PerfRun { @@ -28,9 +28,8 @@ export default class PerfRun { duration: number; @Field(() => [Statistics], { - nullable: false, + nullable: true, description: 'Statistics coming from JMeter statistics.json file', }) statistics: Statistics[]; - } diff --git a/src/utils/testing/types/statistics.ts b/src/utils/testing/types/statistics.ts index 588aa04..a58705a 100644 --- a/src/utils/testing/types/statistics.ts +++ b/src/utils/testing/types/statistics.ts @@ -3,87 +3,86 @@ import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() export default class Statistics { @Field(() => String, { - nullable: false, + nullable: true, description: 'Name of the transaction', }) transaction: string; @Field({ - nullable: false, + nullable: true, description: 'sampleCount result', }) - sampleCount: number; + sampleCount: number; @Field({ - nullable: false, + nullable: true, description: 'errorCount result', }) - errorCount: number; + errorCount: number; @Field({ - nullable: false, + nullable: true, description: 'errorPct result', }) - errorPct: number; + errorPct: number; @Field({ - nullable: false, + nullable: true, description: 'meanResTime result', }) - meanResTime: number; + meanResTime: number; @Field({ - nullable: false, + nullable: true, description: 'medianResTime result', }) - medianResTime: number; + medianResTime: number; @Field({ - nullable: false, + nullable: true, description: 'minResTime result', }) - minResTime: number; + minResTime: number; @Field({ - nullable: false, + nullable: true, description: 'maxResTime result', }) - maxResTime: number; + maxResTime: number; @Field({ - nullable: false, + nullable: true, description: 'pct1ResTime result', }) - pct1ResTime: number; + pct1ResTime: number; @Field({ - nullable: false, + nullable: true, description: 'pct2ResTime result', }) - pct2ResTime: number; + pct2ResTime: number; @Field({ - nullable: false, + nullable: true, description: 'pct3ResTime result', }) - pct3ResTime: number; + pct3ResTime: number; @Field({ - nullable: false, + nullable: true, description: 'throughput result', }) - throughput: number; + throughput: number; @Field({ - nullable: false, + nullable: true, description: 'receivedKBytesPerSec result', }) - receivedKBytesPerSec: number; + receivedKBytesPerSec: number; @Field({ - nullable: false, + nullable: true, description: 'sentKBytesPerSec result', }) - sentKBytesPerSec: number; - + sentKBytesPerSec: number; } diff --git a/src/utils/testing/types/tag.ts b/src/utils/testing/types/tag.ts new file mode 100644 index 0000000..8b0719e --- /dev/null +++ b/src/utils/testing/types/tag.ts @@ -0,0 +1,15 @@ +import { Field, ObjectType, ID } from '@nestjs/graphql'; + +@ObjectType() +export default class Tag { + @Field(() => ID, { + nullable: true, + }) + id: string; + + @Field(() => String, { + nullable: false, + description: 'Name of the tag', + }) + name: string; +} diff --git a/src/utils/testing/types/tagConnection.ts b/src/utils/testing/types/tagConnection.ts new file mode 100644 index 0000000..bb84b42 --- /dev/null +++ b/src/utils/testing/types/tagConnection.ts @@ -0,0 +1,18 @@ +import { Field, ObjectType, Int } from '@nestjs/graphql'; + +import TagEdge from './tagEdge'; + +@ObjectType() +export default class TagConnection { + @Field(() => [TagEdge], { + nullable: false, + description: 'A list of edges.', + }) + edges: TagEdge[]; + + @Field(() => Int, { + nullable: false, + description: 'Identifies the total count of items in the connection.', + }) + totalCount: string; +} diff --git a/src/utils/testing/types/tagEdge.ts b/src/utils/testing/types/tagEdge.ts new file mode 100644 index 0000000..a1ea7a0 --- /dev/null +++ b/src/utils/testing/types/tagEdge.ts @@ -0,0 +1,12 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import Tag from './tag'; + +@ObjectType() +export default class TagEdge { + @Field(() => Tag, { + nullable: false, + description: 'The item at the end of the edge.', + }) + node: Tag; +}