diff --git a/redisinsight/ui/src/packages/ri-explain/src/Explain.tsx b/redisinsight/ui/src/packages/ri-explain/src/Explain.tsx index 649afcbb06..740bdd3c5c 100644 --- a/redisinsight/ui/src/packages/ri-explain/src/Explain.tsx +++ b/redisinsight/ui/src/packages/ri-explain/src/Explain.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef } from 'react' import { Model, Graph } from '@antv/x6' -import { register} from '@antv/x6-react-shape' +import { register } from '@antv/x6-react-shape' import Hierarchy from '@antv/hierarchy' import { formatRedisReply } from 'redisinsight-plugin-sdk' @@ -24,15 +24,16 @@ import { ParseExplain, ParseGraphV2, ParseProfile, - ParseProfileCluster, GetAncestors, GetTotalExecutionTime, + transformProfileResult, + findFlatProfile, } from './parser' import { ExplainNode, ProfileNode } from './Node' interface IExplain { command: string - data: [{response: string[] | string | any}] + data: [{ response: string[] | string | any }] } function getEdgeSize(c: number) { @@ -50,11 +51,11 @@ function getEdgeColor(isDarkTheme: boolean) { export default function Explain(props: IExplain): JSX.Element { const command = props.command.split(' ')[0].toLowerCase() if (command.startsWith('graph')) { - const info = props.data[0].response + const info = props.data[0].response const resp = ParseGraphV2(info) let profilingTime: IProfilingTime = {} - let t = command.endsWith('explain') ? CoreType.Explain : CoreType.Profile + const t = command.endsWith('explain') ? CoreType.Explain : CoreType.Profile if (t === CoreType.Profile) { profilingTime = { 'Total Execution Time': GetTotalExecutionTime(resp) @@ -76,7 +77,7 @@ export default function Explain(props: IExplain): JSX.Element { const [parsedRedisReply, setParsedRedisReply] = useState('') useEffect(() => { - if (command == 'ft.profile') { + if (command === 'ft.profile') { const getParsedResponse = async () => { const formattedResponse = await formatRedisReply(props.data[0].response, props.command) setParsedRedisReply(formattedResponse) @@ -85,54 +86,40 @@ export default function Explain(props: IExplain): JSX.Element { } }) - if (command == 'ft.profile') { - const info = props.data[0].response[1] + if (command === 'ft.profile') { + try { + const { data } = props + const isNewResponse = typeof data[0].response[1]?.[0] === 'string' - let data: EntityInfo - let profilingTime: IProfilingTime = {} + const [, profiles] = data[0].response || [] + const transformedProfiles = isNewResponse ? profiles : transformProfileResult(profiles) + const [shard] = findFlatProfile('Shards', transformedProfiles) + const profileInfo: EntityInfo = ParseProfile(shard) + + const profilingTime = { + 'Total Profile Time': findFlatProfile('Total Profile Time', shard), + 'Parsing Time': findFlatProfile('Parsing Time', shard), + 'Pipeline Creation Time': findFlatProfile('Pipeline Creation Time', shard), + } - if (info.length > 5 && typeof info[0] === 'string' && info[0].toLowerCase().startsWith('shard')) { - let [cluster, entityInfo] = ParseProfileCluster(info) - cluster['Coordinator'].forEach((kv: [string, string]) => profilingTime[kv[0]] = kv[1]) - data = entityInfo return ( - <> -
Visualization is not supported for a clustered database.
-
{parsedRedisReply}
- + ) - } else if (typeof info[0] === 'string' && info[0].toLowerCase().startsWith('coordinator')) { - const resultsProfile = info[2] - data = ParseProfile(resultsProfile) - profilingTime = { - 'Total Coordinator time': info[4], - 'Total profile time': resultsProfile[0][1], - 'Parsing time': resultsProfile[1][1], - 'Pipeline creation time': resultsProfile[2][1], - } + } catch (e) { + console.error(e) + return ( <> -
Visualization is not supported for a clustered database.
+
Some error happened during parsing the data.
{parsedRedisReply}
) - } else { - data = ParseProfile(info) - profilingTime = { - 'Total Profile Time': info[0][1], - 'Parsing Time': info[1][1], - 'Pipeline Creation Time': info[2][1], - } } - - return ( - - ) } const resp = props.data[0].response @@ -169,7 +156,7 @@ interface IProfilingTime { [key: string]: string } -function ExplainDraw({data, type, module, profilingTime}: {data: any, type: CoreType, module: ModuleType, profilingTime?: IProfilingTime}): JSX.Element { +function ExplainDraw({ data, type, module, profilingTime }: { data: any, type: CoreType, module: ModuleType, profilingTime?: IProfilingTime }): JSX.Element { const container = useRef(null) const [done, setDone] = useState(false) @@ -186,7 +173,7 @@ function ExplainDraw({data, type, module, profilingTime}: {data: any, type: Core const height = Math.max((b?.height || 585) + 100, parent.document.body.offsetHeight) if (type !== CoreType.Profile && collapse) { core?.resize(width, window.outerHeight - 250) - core?.positionContent("top") + core?.positionContent('top') } else { core?.resize(width, height) } @@ -194,7 +181,7 @@ function ExplainDraw({data, type, module, profilingTime}: {data: any, type: Core setIsFullScreen(false) if (type !== CoreType.Profile && collapse) { core?.resize(width, 400) - core?.positionContent("top") + core?.positionContent('top') } else { core?.resize(width, (b?.height || 585) + 100) } @@ -203,7 +190,6 @@ function ExplainDraw({data, type, module, profilingTime}: {data: any, type: Core window.addEventListener('resize', resize) useEffect(() => { - if (done) return setDone(true) @@ -223,14 +209,14 @@ function ExplainDraw({data, type, module, profilingTime}: {data: any, type: Core setCore(graph) - graph.on("resize", () => graph.centerContent()) - graph.on("node:mouseenter", x => { - const {id} = x.node.getData() + graph.on('resize', () => graph.centerContent()) + graph.on('node:mouseenter', (x) => { + const { id } = x.node.getData() // Find ancestors of a node - const ancestors = GetAncestors(data, id, {found: false, pairs: []}) - ancestors.pairs.forEach(p => { + const ancestors = GetAncestors(data, id, { found: false, pairs: [] }) + ancestors.pairs.forEach((p) => { // Highlight ancestor and their ancestor - document.querySelector(`#node-${p[0]}`)?.setAttribute("style", "outline: 1px solid #85A2FE !important;") + document.querySelector(`#node-${p[0]}`)?.setAttribute('style', 'outline: 1px solid #85A2FE !important;') // Get edge size of parent ancestor to apply the right edge stroke const edge = graph.getCellById(`${p[0]}-${p[1]}`) const edgeColor = '#85A2FE' @@ -243,11 +229,11 @@ function ExplainDraw({data, type, module, profilingTime}: {data: any, type: Core }) }) - graph.on("node:mouseleave", x => { - const {id} = x.node.getData() - const ancestors = GetAncestors(data, id, {found: false, pairs: []}) - ancestors.pairs.forEach(p => { - document.querySelector(`#node-${p[0]}`)?.setAttribute("style", "") + graph.on('node:mouseleave', (x) => { + const { id } = x.node.getData() + const ancestors = GetAncestors(data, id, { found: false, pairs: [] }) + ancestors.pairs.forEach((p) => { + document.querySelector(`#node-${p[0]}`)?.setAttribute('style', '') const edge = graph.getCellById(`${p[0]}-${p[1]}`) const edgeColor = getEdgeColor(isDarkTheme) edge.setAttrs({ @@ -315,13 +301,12 @@ function ExplainDraw({data, type, module, profilingTime}: {data: any, type: Core } } - - const portId = data.id + '-source' - let targetPort = {} + const portId = `${data.id}-source` + const targetPort = {} const targetItem: any = [] if (info.parentId) { - targetItem.push({id: `${info.id}-${info.parentId}-target`, group: `${info.parentId}-target`}) - targetPort[info.parentId+'-target'] = { + targetItem.push({ id: `${info.id}-${info.parentId}-target`, group: `${info.parentId}-target` }) + targetPort[`${info.parentId}-target`] = { position: { name: 'bottom' }, attrs: { circle: { @@ -355,7 +340,7 @@ function ExplainDraw({data, type, module, profilingTime}: {data: any, type: Core ...targetPort, }, items: [ - ...data.children.map(c => ({ + ...data.children.map((c) => ({ id: `${data.id}-${c.id}`, group: portId })), ...targetItem, @@ -409,63 +394,59 @@ function ExplainDraw({data, type, module, profilingTime}: {data: any, type: Core graph.fromJSON(model) graph.centerContent() - }, [done]) - const ele = document.querySelector("#container-parent") - - let pos = { top: 0, left: 0, x: 0, y: 0 } + const ele = document.querySelector('#container-parent') - const mouseMoveHandler = function (e) { - // How far the mouse has been moved - const dx = e.clientX - pos.x - const dy = e.clientY - pos.y - - // Scroll the element - if (ele) { - ele.scrollTop = pos.top - dy - ele.scrollLeft = pos.left - dx - } - } + let pos = { top: 0, left: 0, x: 0, y: 0 } + const mouseMoveHandler = function (e) { + // How far the mouse has been moved + const dx = e.clientX - pos.x + const dy = e.clientY - pos.y - const mouseUpHandler = function () { - document.removeEventListener('mousemove', mouseMoveHandler) - document.removeEventListener('mouseup', mouseUpHandler) + // Scroll the element + if (ele) { + ele.scrollTop = pos.top - dy + ele.scrollLeft = pos.left - dx } + } + const mouseUpHandler = function () { + document.removeEventListener('mousemove', mouseMoveHandler) + document.removeEventListener('mouseup', mouseUpHandler) + } - const mouseDownHandler = function (e) { - pos = { - // The current scroll - left: ele?.scrollLeft || 0, - top: ele?.scrollTop || 0, - // Get the current mouse position - x: e.clientX, - y: e.clientY, - } - - document.addEventListener('mousemove', mouseMoveHandler) - setTimeout(() => document.addEventListener('mouseup', mouseUpHandler), 100) + const mouseDownHandler = function (e) { + pos = { + // The current scroll + left: ele?.scrollLeft || 0, + top: ele?.scrollTop || 0, + // Get the current mouse position + x: e.clientX, + y: e.clientY, } - ele?.addEventListener('mousedown', mouseDownHandler) + document.addEventListener('mousemove', mouseMoveHandler) + setTimeout(() => document.addEventListener('mouseup', mouseUpHandler), 100) + } + ele?.addEventListener('mousedown', mouseDownHandler) if (type !== CoreType.Profile && collapse) { core?.resize(undefined, isFullScreen ? (window.outerHeight - 250) : 400) - core?.positionContent("top") + core?.positionContent('top') } else { core?.resize(undefined, core?.getContentBBox().height + 100) } return (
- { type !== CoreType.Profile && collapse &&
} + { type !== CoreType.Profile && collapse &&
}
( + ].map((item) => ( )} - { type !== CoreType.Profile && + { type !== CoreType.Profile + && (
{ + onClick={(e) => { e.preventDefault() setTimeout(() => document.addEventListener('mouseup', mouseUpHandler), 100) - if (!collapse) { // About to collapse? + if (!collapse) { // About to collapse? core?.zoomTo(1) core?.resize(undefined, core?.getContentBBox().height + 50) } @@ -535,26 +517,28 @@ function ExplainDraw({data, type, module, profilingTime}: {data: any, type: Core > { collapse - ? - <> -
Expand
- - - : - <> -
Collapse
- - + ? ( + <> +
Expand
+ + + ) + : ( + <> +
Collapse
+ + + ) }
- } - { profilingTime && - ( - module === ModuleType.Search && - ( + )} + { profilingTime + && ( + module === ModuleType.Search + && (
{ - Object.keys(profilingTime).map(key => ( + Object.keys(profilingTime).map((key) => (
{profilingTime[key]}
{key}
diff --git a/redisinsight/ui/src/packages/ri-explain/src/main.tsx b/redisinsight/ui/src/packages/ri-explain/src/main.tsx index 8e630406ab..fea8ea0658 100644 --- a/redisinsight/ui/src/packages/ri-explain/src/main.tsx +++ b/redisinsight/ui/src/packages/ri-explain/src/main.tsx @@ -13,11 +13,16 @@ import { icon as EuiIconArrowUp } from '@elastic/eui/es/components/icon/assets/a import { icon as EuiIconArrowDown } from '@elastic/eui/es/components/icon/assets/arrow_down' import { App } from './App' import './styles/styles.scss' -import result from '../mockData/resultExplain.json' + +// import data from '../test-data/result-explain.json' +// import data from '../test-data/result-profile_r7.json' +import data from '../test-data/result-profile_r7--aggregate.json' +// import data from '../test-data/result-profile_r8.json' interface Props { command?: string data?: { response: any, status: string }[] + mode?: string } appendIconComponentCache({ @@ -41,8 +46,9 @@ const renderCore = (props: Props) => renderApp( ) if (process.env.NODE_ENV === 'development') { - const command = 'GRAPH.EXPLAIN us_government "MATCH (p:President)-[:BORN]->(h:State {name:\'Hawaii\'}) RETURN p"' - renderCore({ command, data: result, mode: 'ASCII' }) + const command = 'FT.PROFILE \'idx:bicycle\' SEARCH QUERY \'*\' NOCONTENT' + + renderCore({ command, data, mode: 'ASCII' }) } // This is a required action - export the main function for execution of the visualization diff --git a/redisinsight/ui/src/packages/ri-explain/src/parser.ts b/redisinsight/ui/src/packages/ri-explain/src/parser.ts index baa268273f..d9f4f810e2 100644 --- a/redisinsight/ui/src/packages/ri-explain/src/parser.ts +++ b/redisinsight/ui/src/packages/ri-explain/src/parser.ts @@ -45,14 +45,14 @@ enum TokenType { } class Token { - T: TokenType - Data: string + T: TokenType - constructor(t: TokenType, data: string) { - this.T = t - this.Data = data - } + Data: string + constructor(t: TokenType, data: string) { + this.T = t + this.Data = data + } } const KEYWORDS = { @@ -68,12 +68,14 @@ const KEYWORDS = { [TokenType.TAG.toString()]: TokenType.TAG, [TokenType.NUMERIC.toString()]: TokenType.NUMERIC, - 'inf': TokenType.NUMBER, + inf: TokenType.NUMBER, } class Lexer { Input: string + Position: number + ReadPosition: number C?: string @@ -99,9 +101,8 @@ class Lexer { PeekChar() { if (this.ReadPosition >= this.Input.length) { return null - } else { - return this.Input[this.ReadPosition] } + return this.Input[this.ReadPosition] } SkipWhitespace() { @@ -127,26 +128,26 @@ class Lexer { while ( this.C !== undefined && ( - isLetter(this.C) || - ['@', ':', '\\'].includes(this.C) || - (startsWithAt && isDigit(this.C)) || + isLetter(this.C) + || ['@', ':', '\\'].includes(this.C) + || (startsWithAt && isDigit(this.C)) // Text can be searched in multiple schemas via '|' // // Example: // FT.CREATE idx SCHEMA t1 TEXT t2 TEXT // FT.EXPLAIN idx '@t1|t2:(text value)' - (startsWithAt && this.C === '|') || - str.startsWith('TAG:@') && isDigit(this.C) || - prevEscape + || (startsWithAt && this.C === '|') + || str.startsWith('TAG:@') && isDigit(this.C) + || prevEscape ) ) { - str = str + this.C + str += this.C if (this.C === '\\' && this.PeekChar() === '\\') { // '\' appears twice query result when escaped a character. // // For example, if space has to be escaped, instead of '\ ', you will find '\\ '. - this.ReadChar() // read of extra '\' + this.ReadChar() // read of extra '\' prevEscape = true } else { prevEscape = false @@ -159,7 +160,7 @@ class Lexer { ReadNumber(): string { let str = '' while (this.C !== undefined && (isDigit(this.C) || this.C === '.') && !Number.isNaN(parseFloat(str + this.C))) { - str = str + this.C + str += this.C this.ReadChar() } return str @@ -191,22 +192,22 @@ class Lexer { break case '-':// TODO: This should be MINUS token t = new Token(TokenType.IDENTIFIER, this.C) - let p = this.PeekChar() - if (p !== null && isDigit(p)){ + const p = this.PeekChar() + if (p !== null && isDigit(p)) { this.ReadChar() const n = this.ReadNumber() - t = new Token(TokenType.NUMBER, '-' + n) + t = new Token(TokenType.NUMBER, `-${n}`) return t } break case ',': t = new Token(TokenType.COMMA, this.C) break - case '.': - t = new Token(TokenType.DOT, this.C) - break + case '.': + t = new Token(TokenType.DOT, this.C) + break case '<': - let lPeekChar = this.PeekChar() + const lPeekChar = this.PeekChar() if (lPeekChar !== null && lPeekChar === '=') { t = new Token(TokenType.LESS_EQUAL, '<=') this.ReadChar() @@ -215,7 +216,7 @@ class Lexer { } break case '>': - let rPeekChar = this.PeekChar() + const rPeekChar = this.PeekChar() if (rPeekChar !== null && rPeekChar === '=') { t = new Token(TokenType.GREATER_EQUAL, '>=') this.ReadChar() @@ -224,7 +225,7 @@ class Lexer { } break case '=': - let ePeekChar = this.PeekChar() + const ePeekChar = this.PeekChar() if (ePeekChar !== null && ePeekChar === '=') { t = new Token(TokenType.EQUAL, '==') this.ReadChar() @@ -269,13 +270,12 @@ class Lexer { } t = new Token(tokenType, literal) return t - } else if (this.C !== undefined && isDigit(this.C)) { + } if (this.C !== undefined && isDigit(this.C)) { const n = this.ReadNumber() t = new Token(TokenType.NUMBER, n) return t - } else { - t = new Token(TokenType.ILLEGAL, this.C) } + t = new Token(TokenType.ILLEGAL, this.C) } this.ReadChar() return t @@ -312,7 +312,6 @@ export enum EntityType { CLUSTER_MERGE = 'CLUSTER MERGE' } - export interface EntityInfo { id: string type: EntityType, @@ -340,29 +339,30 @@ export function GetAncestors(info: EntityInfo, searchId: string, a: IAncestors): found: true, pairs: info.parentId ? [[info.parentId, info.id]] : [] } - } else { - let r: IAncestors = {...a} - for (let i = 0; i < info.children.length; i++) { - let c = info.children[i] - let ci = GetAncestors(c, searchId, a) - if (ci.found) { - r.found = true - r.pairs = [...a.pairs, ...ci.pairs] - if (info.parentId) { - r.pairs = [...r.pairs, [info.parentId, info.id]] - } - return r + } + const r: IAncestors = { ...a } + for (let i = 0; i < info.children.length; i++) { + const c = info.children[i] + const ci = GetAncestors(c, searchId, a) + if (ci.found) { + r.found = true + r.pairs = [...a.pairs, ...ci.pairs] + if (info.parentId) { + r.pairs = [...r.pairs, [info.parentId, info.id]] } + return r } - return r } + return r } - class Expr { Core: string + SubType: EntityType + Time?: string + Info?: string constructor(expr: string, subType: EntityType, info: string | undefined = undefined) { @@ -372,8 +372,7 @@ class Expr { } toJSON(): EntityInfo { - - let snippet: string | undefined = undefined + let snippet: string | undefined if (this.SubType === EntityType.TAG && this.Info?.startsWith('TAG:')) { snippet = this.Info?.substr(4) @@ -390,7 +389,7 @@ class Expr { // snippet: this.Core, type: EntityType.Expr, subType: this.SubType, - snippet: snippet, + snippet, data: this.Core, children: [], time: this.Time, @@ -400,13 +399,14 @@ class Expr { class NumericExpr { Left: number + LSign: Token Identifier: Token Right: number - RSign: Token + RSign: Token constructor(left: number, lsign: Token, identifier: Token, rsign: Token, right: number) { this.Left = left @@ -433,7 +433,9 @@ type ExprTuple2 = SearchExpr[] class ExpandExpr { Type: EntityType + Info?: string + Core: ExprTuple2 constructor(type: EntityType, e: ExprTuple2, info?: string) { @@ -445,7 +447,7 @@ class ExpandExpr { toJSON(): EntityInfo { const id = uuidv4() - let snippet: string | undefined = undefined + let snippet: string | undefined if (this.Type === EntityType.TAG && this.Info?.startsWith('TAG:')) { snippet = this.Info?.substr(4) @@ -459,7 +461,7 @@ class ExpandExpr { id, type: this.Type, snippet, - children: this.Core.map(x => x.toJSON()).map((d: EntityInfo) => ({ + children: this.Core.map((x) => x.toJSON()).map((d: EntityInfo) => ({ ...d, parentId: id, parentSnippet: snippet, @@ -468,11 +470,13 @@ class ExpandExpr { } } - class Parser { private L: Lexer + CurrentToken: Token + PeekToken: Token + Errors: string[] constructor(l: Lexer) { @@ -513,18 +517,17 @@ class Parser { // Example: // { { ... } ( { ... } ...) } parseExpandExpr(t: EntityType): ExpandExpr { - assertExpandEntity(t) this.assertToken(t as unknown as TokenType) - let data = this.CurrentToken.Data + const data = this.CurrentToken.Data this.nextToken() this.assertToken(TokenType.LBRACE) - let Exprs: SearchExpr[] = [] + const Exprs: SearchExpr[] = [] this.nextToken() this.assertToken(TokenType.NEW_LINE) @@ -532,14 +535,12 @@ class Parser { this.nextToken() while (true) { - if (this.CurrentToken.T === TokenType.RBRACE && this.PeekToken.T === TokenType.NEW_LINE) { - this.nextToken() break } - const t = this.CurrentToken.T; + const t = this.CurrentToken.T if (this.CurrentToken?.T === TokenType.NUMERIC) { Exprs.push(this.parseNumericExpr()) @@ -586,7 +587,7 @@ class Parser { this.assertToken(TokenType.IDENTIFIER) - let first = this.CurrentToken.Data + const first = this.CurrentToken.Data this.nextToken() @@ -602,7 +603,7 @@ class Parser { this.nextToken() - let second = this.CurrentToken.Data + const second = this.CurrentToken.Data this.nextToken() @@ -620,14 +621,13 @@ class Parser { this.nextToken() - let ids: number[] = [] + const ids: number[] = [] while (this.CurrentToken.T !== TokenType.RBRACE) { ids.push(parseInt(this.CurrentToken.Data)) this.nextToken() - this.assertToken(TokenType.COMMA) this.nextToken() @@ -660,18 +660,16 @@ class Parser { // TODO: Once fixed by redisearch team, remove this. this.assertToken(TokenType.RBRACE) - - return new Expr("", EntityType.WILDCARD) + return new Expr('', EntityType.WILDCARD) } parseExpr() { - this.assertToken(TokenType.IDENTIFIER) let str = '' while (this.CurrentToken.T !== TokenType.NEW_LINE) { - str = str + this.CurrentToken.Data + str += this.CurrentToken.Data this.nextToken() } @@ -681,7 +679,6 @@ class Parser { // Parse a very simple entity with format: // { } parseSimpleExpr(e: EntityType) { - assertSimpleEntity(e) this.assertToken(TokenType[e]) @@ -694,7 +691,7 @@ class Parser { this.assertToken(TokenType.IDENTIFIER) - let identifierData = this.CurrentToken.Data; + const identifierData = this.CurrentToken.Data this.nextToken() @@ -708,13 +705,13 @@ class Parser { parseGeoExpr() { this.assertToken(TokenType.GEO_EXPR) - let geoData = this.CurrentToken.Data + const geoData = this.CurrentToken.Data this.nextToken() this.assertToken(TokenType.IDENTIFIER) - let identifierData = this.CurrentToken.Data + const identifierData = this.CurrentToken.Data this.nextToken() @@ -724,7 +721,7 @@ class Parser { this.assertToken(TokenType.NUMBER) - let first = this.CurrentToken.Data; + const first = this.CurrentToken.Data this.nextToken() @@ -734,19 +731,19 @@ class Parser { this.assertToken(TokenType.NUMBER) - let second = this.CurrentToken.Data; + const second = this.CurrentToken.Data this.nextToken() this.assertToken(TokenType.IDENTIFIER) - assert(this.CurrentToken.Data === '-', "Expected Identifier to be MINUS") + assert(this.CurrentToken.Data === '-', 'Expected Identifier to be MINUS') this.nextToken() this.assertToken(TokenType.IDENTIFIER) - assert(this.CurrentToken.Data === '-', "Expected Identifier to be MINUS") + assert(this.CurrentToken.Data === '-', 'Expected Identifier to be MINUS') this.nextToken() @@ -756,13 +753,13 @@ class Parser { this.assertToken(TokenType.NUMBER) - let third = this.CurrentToken.Data; + const third = this.CurrentToken.Data this.nextToken() this.assertToken(TokenType.IDENTIFIER) - let metric = this.CurrentToken.Data; + const metric = this.CurrentToken.Data this.nextToken() @@ -784,38 +781,35 @@ class Parser { this.assertToken(TokenType.NUMBER) - let left = this.CurrentToken?.Data + const left = this.CurrentToken?.Data this.nextToken() - let lsign = this.CurrentToken // TODO: Check sign + const lsign = this.CurrentToken // TODO: Check sign this.nextToken() this.assertToken(TokenType.IDENTIFIER) - let identifier = this.CurrentToken + const identifier = this.CurrentToken this.nextToken() while (this.CurrentToken.T === TokenType.IDENTIFIER) { - identifier.Data = identifier.Data + this.CurrentToken.Data + identifier.Data += this.CurrentToken.Data this.nextToken() } - - let rsign = this.CurrentToken + const rsign = this.CurrentToken this.nextToken() - this.assertToken(TokenType.NUMBER) - let right = this.CurrentToken?.Data + const right = this.CurrentToken?.Data this.nextToken() - this.assertToken(TokenType.RBRACE) this.nextToken()// read off RBRACE @@ -828,17 +822,16 @@ class Parser { } } - function Parse(data: string): SearchExpr { const l = new Lexer(data) - let p = new Parser(l) + const p = new Parser(l) - const t = p.CurrentToken.T; + const t = p.CurrentToken.T if (p.CurrentToken?.T === TokenType.NUMERIC) { return p.parseNumericExpr() - } else if ([ + } if ([ TokenType.UNION, TokenType.INTERSECT, TokenType.NOT, @@ -848,35 +841,32 @@ function Parse(data: string): SearchExpr { TokenType.TAG, ].includes(t)) { return p.parseExpandExpr(EntityType[t]) - } else if (p.CurrentToken.T === TokenType.GEO_EXPR) { + } if (p.CurrentToken.T === TokenType.GEO_EXPR) { return p.parseGeoExpr() - } else if ([TokenType.FUZZY, TokenType.WILDCARD, TokenType.PREFIX].includes(t)) { + } if ([TokenType.FUZZY, TokenType.WILDCARD, TokenType.PREFIX].includes(t)) { return p.parseSimpleExpr(EntityType[t]) - } else if (p.CurrentToken.T === TokenType.IDS_EXPR) { + } if (p.CurrentToken.T === TokenType.IDS_EXPR) { return p.parseIdsExpr() - } else if (p.CurrentToken.T === TokenType.LEXRANGE_EXPR) { + } if (p.CurrentToken.T === TokenType.LEXRANGE_EXPR) { return p.parseLexrangeExpr() - } else if (p.CurrentToken.T === TokenType.LESS) { + } if (p.CurrentToken.T === TokenType.LESS) { return p.parseWildcardEmpty() - } else { - return p.parseExpr() } + return p.parseExpr() } export function ParseExplain(output: string) { return Parse(output).toJSON() } - function isLetter(str: string): boolean { return str.length === 1 && (str.match(/[a-z]/i) !== null) } function isDigit(str: string): boolean { - return str >='0' && str <= '9' + return str >= '0' && str <= '9' } - function assert(c: boolean, errorMsg: string) { if (!c) { throw new Error(errorMsg) @@ -885,7 +875,7 @@ function assert(c: boolean, errorMsg: string) { function assertToken(expected: TokenType, actual: TokenType | undefined) { if (actual === undefined) { - throw new Error("Token is undefined") + throw new Error('Token is undefined') } assert(expected === actual, `Expected ${expected}, Actual: ${actual}`) @@ -905,7 +895,6 @@ function assertExpandEntity(t: EntityType) { } } - function assertSimpleEntity(t: EntityType) { if (![ EntityType.FUZZY, @@ -917,27 +906,26 @@ function assertSimpleEntity(t: EntityType) { } export function ParseProfileCluster(info: any[]): [Object, EntityInfo] { - - let clusterInfo: {[key: string]: any[]} = {} + const clusterInfo: { [key: string]: any[] } = {} let key: string = '' let i = 0 while (i < info.length) { if (Array.isArray(info[i])) { clusterInfo[key].push(info[i]) - } else if (typeof(info[i]) === 'string') { + } else if (typeof (info[i]) === 'string') { key = info[i] clusterInfo[key] = [] } else { - throw new Error("Expected array or string - " + JSON.stringify(info)) + throw new Error(`Expected array or string - ${JSON.stringify(info)}`) } i++ } - let shards: EntityInfo[] = [] + const shards: EntityInfo[] = [] - Object.keys(clusterInfo).map(k => { + Object.keys(clusterInfo).map((k) => { if (k.toLowerCase().startsWith('shard')) { - let shardProfileInfo = ParseProfile(clusterInfo[k]) + const shardProfileInfo = ParseProfile(clusterInfo[k]) shards.push({ id: uuidv4(), type: k as EntityType, @@ -952,59 +940,55 @@ export function ParseProfileCluster(info: any[]): [Object, EntityInfo] { id: uuidv4(), type: EntityType.CLUSTER_MERGE, // children: shards, - children: Object.keys(clusterInfo).filter(k => k.toLowerCase().startsWith('shard')).map(k => - ParseProfile(clusterInfo[k]) - ) + children: Object.keys(clusterInfo).filter((k) => k.toLowerCase().startsWith('shard')).map((k) => + ParseProfile(clusterInfo[k])) } ] } -export function ParseProfile(info: any[][]): EntityInfo { - const parserData: any = info[info.length - 2] - let resp = parserData[0].toLowerCase().startsWith('iterators') ? ParseIteratorProfile(parserData[1]) : null - - const processorsProfile: string[][] = info[info.length - 1].slice(1) +export function ParseProfile(shard: Array): EntityInfo { + const iterators = findFlatProfile('Iterators profile', shard) + let result = iterators ? ParseIteratorProfile(iterators) : null + const processorsProfile: string[][] = findFlatProfile('Result processors profile', shard) for (let i = 0; i < processorsProfile.length; i++) { const e = processorsProfile[i] - let id = uuidv4() - resp = { + const id = uuidv4() + result = { id, type: e[1] as EntityType, time: e[3], counter: e[5], - children: resp ? [{...resp, parentId: id}] : [], + children: result ? [{ ...result, parentId: id }] : [], } } - return resp as EntityInfo + return result as EntityInfo } export function ParseIteratorProfile(data: any[]): EntityInfo { - - let props: {[key: string]: any} = {} + const props: { [key: string]: any } = {} // Parse items with the following format [key1, value1, key2, value2, null, key3, value3, key4, value4_1[], value4_2[]] for (let x = 0; x < data.length; x += 2) { let key = data[x] if (key === null) { - while (data[x] === null) { - x = x + 1 + x += 1 } key = data[x] } let val = data[x + 1] - while (data[x + 1] === null) x = x + 1 + while (data[x + 1] === null) x += 1 val = data[x + 1] if (Array.isArray(val)) { - let arr: any[] = [] + const arr: any[] = [] while ((x + 1) < data.length && Array.isArray(data[x + 1])) { arr.push(data[x + 1]) - x = x + 1 + x += 1 } props[key] = arr } else { @@ -1012,17 +996,17 @@ export function ParseIteratorProfile(data: any[]): EntityInfo { } } - let childrens = props['Child iterators'] || props['Child Iterators'] || [] + const childrens = props['Child iterators'] || props['Child Iterators'] || [] const id = uuidv4() return { id, - type: props['Type'] || props['TYPE'], - time: props['Time'], - counter: props['Counter'], - size: props['Size'], - data: props['Term'], - children: childrens.map(ParseIteratorProfile).map((d: EntityInfo) => ({...d, parentId: id})), + type: props.Type || props.TYPE, + time: props.Time, + counter: props.Counter, + size: props.Size, + data: props.Term, + children: childrens.map(ParseIteratorProfile).map((d: EntityInfo) => ({ ...d, parentId: id })), } // const t: EntityType = props['Type'] @@ -1077,13 +1061,13 @@ export function getOutputLevel(output: string) { function ParseEntity(entity: string, children: EntityInfo[]): EntityInfo { const info = entity.trim().split('|') - let time: string | undefined = '', size: string | undefined = '' + let time: string | undefined = ''; let + size: string | undefined = '' const metaData = info.slice(-1)[0].trim() // Is GRAPH.PROFILE output if (metaData.startsWith('Records produced')) { - [size, time] = metaData.trim().split(',') size = size.split(': ')[1] @@ -1105,21 +1089,20 @@ function ParseEntity(entity: string, children: EntityInfo[]): EntityInfo { } } - export function ParseGraphV2(output: string[]) { - const level = getOutputLevel(output[0]) + 1 - let entity = ParseEntity(output[0], []) - let children: EntityInfo[] = [] + const entity = ParseEntity(output[0], []) + const children: EntityInfo[] = [] - let pairs: [number, number][] = [] + const pairs: [number, number][] = [] - let s: number | null = null, e: number | null = null + let s: number | null = null; const + e: number | null = null let i = 1 while (i < output.length) { - let l = getOutputLevel(output[i]) + const l = getOutputLevel(output[i]) if (l === level) { if (s == null) { s = i @@ -1136,15 +1119,48 @@ export function ParseGraphV2(output: string[]) { } for (let k = 0; k < pairs.length; k++) { - let p = pairs[k] - children.push({...ParseGraphV2(output.slice(p[0], p[1])), parentId: entity.id}) + const p = pairs[k] + children.push({ ...ParseGraphV2(output.slice(p[0], p[1])), parentId: entity.id }) } entity.children = children return entity } - export function GetTotalExecutionTime(g: EntityInfo) { return parseFloat(g.time || '') + g.children.reduce((a, c) => a + GetTotalExecutionTime(c), 0) } + +export const findFlatProfile = (key: string, profiles: any) => { + const index = profiles.findIndex((k: string) => k?.toLowerCase?.() === key?.toLowerCase?.()) + return index > -1 ? profiles[index + 1] : undefined +} + +// Helper to find a profile by key (redis < 8) +export const findProfile = (key: string, profiles: Array<[string, any]>, defautlVal: any = []) => { + const [, ...rest] = profiles.find(([k]) => k?.toLowerCase?.() === key?.toLowerCase?.()) || [] + return rest?.length ? rest : [defautlVal] +} + +// transform profile result to be campatible with redis 8+ +export const transformProfileResult = (profiles: Array<[string, any]>,) => [ + 'Shards', + [ + [ + 'Total profile time', + ...findProfile('Total profile time', profiles), + 'Parsing time', + ...findProfile('Parsing time', profiles), + 'Pipeline creation time', + ...findProfile('Pipeline creation time', profiles), + 'Warning', + ...findProfile('Warning', profiles, 'None'), + 'Iterators profile', + ...findProfile('Iterators profile', profiles), + 'Result processors profile', + findProfile('Result processors profile', profiles) + ] + ], + 'Coordinator', + [] +] diff --git a/redisinsight/ui/src/packages/ri-explain/mockData/resultExplain.json b/redisinsight/ui/src/packages/ri-explain/test-data/result-explain.json similarity index 100% rename from redisinsight/ui/src/packages/ri-explain/mockData/resultExplain.json rename to redisinsight/ui/src/packages/ri-explain/test-data/result-explain.json diff --git a/redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r7--aggregate.json b/redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r7--aggregate.json new file mode 100644 index 0000000000..6879d063a6 --- /dev/null +++ b/redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r7--aggregate.json @@ -0,0 +1,59 @@ +[ + { + "status": "success", + "response": [ + [ + 1, + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + [ + [ + "Total profile time", + "0" + ], + [ + "Parsing time", + "0" + ], + [ + "Pipeline creation time", + "0" + ], + [ + "Warning" + ], + [ + "Iterators profile", + [ + "Type", + "WILDCARD", + "Time", + "0", + "Counter", + 10 + ] + ], + [ + "Result processors profile", + [ + "Type", + "Index", + "Time", + "0", + "Counter", + 10 + ] + ] + ] + ] + } +] diff --git a/redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r7.json b/redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r7.json new file mode 100644 index 0000000000..53fc71dec1 --- /dev/null +++ b/redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r7.json @@ -0,0 +1,75 @@ +[ + { + "status": "success", + "response": [ + [ + 10, + "bicycle:1", + "bicycle:2", + "bicycle:4", + "bicycle:5", + "bicycle:0", + "bicycle:6", + "bicycle:7", + "bicycle:9", + "bicycle:3", + "bicycle:8" + ], + [ + [ + "Total profile time", + "1" + ], + [ + "Parsing time", + "2" + ], + [ + "Pipeline creation time", + "3" + ], + [ + "Warning" + ], + [ + "Iterators profile", + [ + "Type", + "WILDCARD", + "Time", + "0", + "Counter", + 10 + ] + ], + [ + "Result processors profile", + [ + "Type", + "Index", + "Time", + "0", + "Counter", + 10 + ], + [ + "Type", + "Scorer", + "Time", + "0", + "Counter", + 10 + ], + [ + "Type", + "Sorter", + "Time", + "0", + "Counter", + 10 + ] + ] + ] + ] + } +] diff --git a/redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r8.json b/redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r8.json new file mode 100644 index 0000000000..c623ef9100 --- /dev/null +++ b/redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r8.json @@ -0,0 +1,73 @@ +[ + { + "status": "success", + "response": [ + [ + 10, + "bicycle:1", + "bicycle:2", + "bicycle:4", + "bicycle:5", + "bicycle:0", + "bicycle:6", + "bicycle:7", + "bicycle:9", + "bicycle:3", + "bicycle:8" + ], + [ + "Shards", + [ + [ + "Total profile time", + "1", + "Parsing time", + "2", + "Pipeline creation time", + "3", + "Warning", + "None", + "Iterators profile", + [ + "Type", + "WILDCARD", + "Time", + "0", + "Counter", + 10 + ], + "Result processors profile", + [ + [ + "Type", + "Index", + "Time", + "0", + "Counter", + 10 + ], + [ + "Type", + "Scorer", + "Time", + "0", + "Counter", + 10 + ], + [ + "Type", + "Sorter", + "Time", + "0", + "Counter", + 10 + ] + ] + ] + ], + "Coordinator", + [] + ] + ] + } +]