diff --git a/esp/src/src-react/components/ECLArchive.tsx b/esp/src/src-react/components/ECLArchive.tsx new file mode 100644 index 00000000000..ceaee4debfc --- /dev/null +++ b/esp/src/src-react/components/ECLArchive.tsx @@ -0,0 +1,119 @@ +import * as React from "react"; +import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react"; +import { WUDetails, IScope } from "@hpcc-js/comms"; +import nlsHPCC from "src/nlsHPCC"; +import { useWorkunitArchive } from "../hooks/workunit"; +import { useWorkunitMetrics } from "../hooks/metrics"; +import { HolyGrail } from "../layouts/HolyGrail"; +import { DockPanel, DockPanelItem, ResetableDockPanel } from "../layouts/DockPanel"; +import { pushUrl } from "../util/history"; +import { ShortVerticalDivider } from "./Common"; +import { ECLArchiveTree } from "./ECLArchiveTree"; +import { ECLArchiveEditor } from "./ECLArchiveEditor"; +import { MetricsPropertiesTables } from "./MetricsPropertiesTables"; + +const scopeFilterDefault: WUDetails.RequestNS.ScopeFilter = { + MaxDepth: 999999, + ScopeTypes: [] +}; + +const nestedFilterDefault: WUDetails.RequestNS.NestedFilter = { + Depth: 999999, + ScopeTypes: ["activity"] +}; + +interface ECLArchiveProps { + wuid: string; + parentUrl?: string; + selection?: string; +} + +export const ECLArchive: React.FunctionComponent = ({ + wuid, + parentUrl = `/workunits/${wuid}/eclsummary`, + selection +}) => { + const [fullscreen, setFullscreen] = React.useState(false); + const [dockpanel, setDockpanel] = React.useState(); + const [_archiveXmlStr, _workunit2, _state2, archive, refreshArchive] = useWorkunitArchive(wuid); + const [metrics, _columns, _activities, _properties, _measures, _scopeTypes, _fetchStatus, refreshMetrics] = useWorkunitMetrics(wuid, scopeFilterDefault, nestedFilterDefault); + const [markers, setMarkers] = React.useState<{ lineNum: number, label: string }[]>([]); + const [selectionText, setSelectionText] = React.useState(""); + const [selectedMetrics, setSelectedMetrics] = React.useState([]); + + selection = selection ?? archive?.queryId(); + + React.useEffect(() => { + if (archive) { + archive?.updateMetrics(metrics); + } + }, [archive, metrics]); + + React.useEffect(() => { + if (metrics.length) { + setSelectionText(archive?.content(selection) ?? ""); + setMarkers(archive?.markers(selection) ?? []); + setSelectedMetrics(archive?.metrics(selection) ?? []); + } + }, [archive, metrics.length, selection]); + + const setSelectedItem = React.useCallback((selId: string) => { + pushUrl(`${parentUrl}/${selId}`); + }, [parentUrl]); + + React.useEffect(() => { + if (dockpanel) { + // Should only happen once on startup --- + const layout: any = dockpanel.layout(); + if (Array.isArray(layout?.main?.sizes) && layout.main.sizes.length === 2) { + layout.main.sizes = [0.3, 0.7]; + dockpanel.layout(layout).lazyRender(); + } + } + }, [dockpanel]); + + // Command Bar --- + const buttons = React.useMemo((): ICommandBarItemProps[] => [ + { + key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" }, + onClick: () => { + refreshArchive(); + refreshMetrics(); + pushUrl(`${parentUrl}`); + } + }, + { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => }, + ], [parentUrl, refreshArchive, refreshMetrics]); + + const rightButtons = React.useMemo((): ICommandBarItemProps[] => [ + { + key: "copy", text: nlsHPCC.CopyToClipboard, disabled: !navigator?.clipboard?.writeText, iconOnly: true, iconProps: { iconName: "Copy" }, + onClick: () => { + navigator?.clipboard?.writeText(selectionText); + } + }, { + key: "fullscreen", title: nlsHPCC.MaximizeRestore, iconProps: { iconName: fullscreen ? "ChromeRestore" : "FullScreen" }, + onClick: () => setFullscreen(!fullscreen) + } + ], [selectionText, fullscreen]); + + return } + main={ + + + { // Only render after archive is loaded (to ensure it "defaults to open") --- + archive?.modAttrs.length && + + } + + + + + + + + + } + />; +}; diff --git a/esp/src/src-react/components/ECLArchiveEditor.tsx b/esp/src/src-react/components/ECLArchiveEditor.tsx new file mode 100644 index 00000000000..8a3c1805128 --- /dev/null +++ b/esp/src/src-react/components/ECLArchiveEditor.tsx @@ -0,0 +1,54 @@ +import * as React from "react"; +import { useConst } from "@fluentui/react-hooks"; +import { Palette } from "@hpcc-js/common"; +import { ECLEditor } from "@hpcc-js/codemirror"; +import { useUserTheme } from "../hooks/theme"; +import { AutosizeHpccJSComponent } from "../layouts/HpccJSAdapter"; + +const palette = Palette.rainbow("YlOrRd"); + +interface ECLArchiveProps { + ecl?: string; + readonly?: boolean; + markers?: { lineNum: number, label: string }[]; +} + +export const ECLArchiveEditor: React.FunctionComponent = ({ + ecl = "", + readonly = true, + markers = [] +}) => { + const { isDark } = useUserTheme(); + + const editor = useConst(() => + new ECLEditor() + .readOnly(true) + ); + + React.useEffect(() => { + editor + ?.text(ecl) + ?.readOnly(readonly) + ?.option("theme", isDark ? "darcula" : "default") + ?.lazyRender() + ; + }, [ecl, editor, isDark, readonly]); + + React.useEffect(() => { + const fontFamily = "Verdana"; + const fontSize = 12; + const maxLabelWidth = Math.max( + ...markers.map(marker => { + const color = palette(+marker.label, 0, 100); + editor?.addGutterMarker(+marker.lineNum - 1, marker.label, color, fontFamily, `${fontSize}px`); + return editor?.textSize(marker.label, fontFamily, fontSize)?.width ?? 0; + }) + ); + editor + ?.gutterMarkerWidth(maxLabelWidth + 6) + ?.lazyRender() + ; + }, [editor, markers]); + + return ; +}; diff --git a/esp/src/src-react/components/ECLArchiveTree.tsx b/esp/src/src-react/components/ECLArchiveTree.tsx new file mode 100644 index 00000000000..af64bb4ab39 --- /dev/null +++ b/esp/src/src-react/components/ECLArchiveTree.tsx @@ -0,0 +1,104 @@ +import * as React from "react"; +import { FlatTree, useHeadlessFlatTree_unstable, HeadlessFlatTreeItemProps, TreeItem, TreeItemLayout, CounterBadge } from "@fluentui/react-components"; +import { FluentIconsProps, FolderOpen20Regular, Folder20Regular, FolderOpen20Filled, Folder20Filled, Document20Regular, Document20Filled, Important16Regular } from "@fluentui/react-icons"; +import { Archive, isAttribute } from "../util/metricArchive"; + +type FlatItem = HeadlessFlatTreeItemProps & { fileTimePct?: number, content: string }; + +const iconStyleProps: FluentIconsProps = { + primaryFill: "red", +}; + +const AsideContent = ({ + isImportant, + messageCount, +}: { + isImportant?: boolean; + messageCount?: number; +}) => { + const color = messageCount < 50 ? "brand" : + messageCount < 70 ? "informative" : + messageCount < 90 ? "important" : + "danger"; + return <> + {isImportant && } + {!isNaN(messageCount) && messageCount > 0 && ( + + )} + ; +}; + +interface ECLArchiveTreeProps { + archive?: Archive; + selectedAttrIDs: string[]; + setSelectedItem: (eclId: string, scopeID: string[]) => void; +} + +export const ECLArchiveTree: React.FunctionComponent = ({ + archive, + selectedAttrIDs = [], + setSelectedItem +}) => { + + const defaultOpenItems = React.useMemo(() => { + return (archive?.modAttrs.filter(modAttr => modAttr.type === "Module") ?? []).map(modAttr => modAttr.id) ?? []; + }, [archive?.modAttrs]); + + const [flatTreeItems, setFlatTreeItems] = React.useState([]); + const flatTree = useHeadlessFlatTree_unstable(flatTreeItems, { defaultOpenItems }); + + React.useEffect(() => { + const flatTreeItems: FlatItem[] = []; + archive?.modAttrs.forEach(modAttr => { + flatTreeItems.push({ + value: modAttr.id, + parentValue: modAttr.parentId ? modAttr.parentId : undefined, + content: modAttr.name, + fileTimePct: isAttribute(modAttr) && Math.round((archive?.sourcePathTime(modAttr.sourcePath) / archive?.timeTotalExecute) * 100), + }); + }); + setFlatTreeItems(flatTreeItems); + }, [archive, archive?.modAttrs, archive?.timeTotalExecute]); + + const onClick = React.useCallback(evt => { + const attrId = evt.currentTarget?.dataset?.fuiTreeItemValue; + const modAttr = archive?.modAttrs.find(modAttr => modAttr.id === attrId); + if (modAttr?.type === "Attribute") { + setSelectedItem(attrId, archive.metricIDs(attrId)); + } + }, [archive, setSelectedItem]); + + const { ...treeProps } = flatTree.getTreeProps(); + return + { + Array.from(flatTree.items(), flatTreeItem => { + console.log(flatTreeItem.getTreeItemProps()); + const { fileTimePct, content, ...treeItemProps } = flatTreeItem.getTreeItemProps(); + return + : : + // selectedItem?.startsWith(content) ? : ) + // : + // undefined + // } + iconBefore={ + flatTreeItem.itemType === "branch" ? + (treeProps.openItems.has(flatTreeItem.value) ? + selectedAttrIDs.some(attrId => attrId.startsWith(content)) ? : : + selectedAttrIDs.some(attrId => attrId.startsWith(content)) ? : ) : + selectedAttrIDs.some(attrId => attrId === flatTreeItem.value) ? + : + + } + aside={} + > + {content} + + ; + }) + } + ; +}; diff --git a/esp/src/src-react/components/Metrics.tsx b/esp/src/src-react/components/Metrics.tsx index f84a5b5bfad..8af6843327c 100644 --- a/esp/src/src-react/components/Metrics.tsx +++ b/esp/src/src-react/components/Metrics.tsx @@ -21,6 +21,7 @@ import { ErrorBoundary } from "../util/errorBoundary"; import { ShortVerticalDivider } from "./Common"; import { MetricsOptions } from "./MetricsOptions"; import { BreadcrumbInfo, OverflowBreadcrumb } from "./controls/OverflowBreadcrumb"; +import { MetricsPropertiesTables } from "./MetricsPropertiesTables"; const logger = scopedLogger("src-react/components/Metrics.tsx"); @@ -360,28 +361,6 @@ export const Metrics: React.FunctionComponent = ({ }, [lineage, selectedLineage]); // Props Table --- - const propsTable = useConst(() => new Table() - .columns([nlsHPCC.Property, nlsHPCC.Value, "Avg", "Min", "Max", "Delta", "StdDev", "SkewMin", "SkewMax", "NodeMin", "NodeMax"]) - .columnWidth("auto") - ); - - const updatePropsTable = React.useCallback((scopes: IScope[]) => { - const props = []; - scopes.forEach((item, idx) => { - for (const key in item.__groupedProps) { - const row = item.__groupedProps[key]; - props.push([row.Key, row.Value, row.Avg, row.Min, row.Max, row.Delta, row.StdDev, row.SkewMin, row.SkewMax, row.NodeMin, row.NodeMax]); - } - if (idx < scopes.length - 1) { - props.push(["------------------------------", "------------------------------"]); - } - }); - propsTable - ?.data(props) - ?.lazyRender() - ; - }, [propsTable]); - const propsTable2 = useConst(() => new Table() .columns([nlsHPCC.Property, nlsHPCC.Value]) .columnWidth("auto") @@ -437,7 +416,6 @@ export const Metrics: React.FunctionComponent = ({ React.useEffect(() => { if (selectedMetrics) { updateScopesTable(selectedMetrics); - updatePropsTable(selectedMetrics); updatePropsTable2(selectedMetrics); updateLineage(selectedMetrics); } @@ -569,7 +547,7 @@ export const Metrics: React.FunctionComponent = ({ /> - + diff --git a/esp/src/src-react/components/MetricsPropertiesTables.tsx b/esp/src/src-react/components/MetricsPropertiesTables.tsx new file mode 100644 index 00000000000..031b5daf8ad --- /dev/null +++ b/esp/src/src-react/components/MetricsPropertiesTables.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; +import { useConst } from "@fluentui/react-hooks"; +import { IScope } from "@hpcc-js/comms"; +import { Table } from "@hpcc-js/dgrid"; +import nlsHPCC from "src/nlsHPCC"; +import { AutosizeHpccJSComponent } from "../layouts/HpccJSAdapter"; + +interface MetricsPropertiesTablesProps { + scopes?: IScope[]; +} + +export const MetricsPropertiesTables: React.FunctionComponent = ({ + scopes = [] +}) => { + + // Props Table --- + const propsTable = useConst(() => new Table() + .columns([nlsHPCC.Property, nlsHPCC.Value, "Avg", "Min", "Max", "Delta", "StdDev", "SkewMin", "SkewMax", "NodeMin", "NodeMax"]) + .columnWidth("auto") + ); + + React.useEffect(() => { + const props = []; + scopes.forEach((item, idx) => { + for (const key in item.__groupedProps) { + const row = item.__groupedProps[key]; + props.push([row.Key, row.Value, row.Avg, row.Min, row.Max, row.Delta, row.StdDev, row.SkewMin, row.SkewMax, row.NodeMin, row.NodeMax]); + } + if (idx < scopes.length - 1) { + props.push(["------------------------------", "------------------------------"]); + } + }); + propsTable + ?.data(props) + ?.lazyRender() + ; + }, [propsTable, scopes]); + + return ; +}; diff --git a/esp/src/src-react/components/WorkunitDetails.tsx b/esp/src/src-react/components/WorkunitDetails.tsx index 2373e601687..d4b779309f2 100644 --- a/esp/src/src-react/components/WorkunitDetails.tsx +++ b/esp/src/src-react/components/WorkunitDetails.tsx @@ -23,6 +23,7 @@ import { Variables } from "./Variables"; import { Workflows } from "./Workflows"; import { WorkunitSummary } from "./WorkunitSummary"; import { TabInfo, DelayLoadedPanel, OverflowTabList } from "./controls/TabbedPanes/index"; +import { ECLArchive } from "./ECLArchive"; const logger = scopedLogger("src-react/components/WorkunitDetails.tsx"); @@ -30,7 +31,7 @@ type StringStringMap = { [key: string]: string }; interface WorkunitDetailsProps { wuid: string; tab?: string; - state?: { outputs?: string, metrics?: string, resources?: string, helpers?: string }; + state?: { outputs?: string, metrics?: string, resources?: string, helpers?: string, eclsummary?: string }; queryParams?: { outputs?: StringStringMap, inputs?: StringStringMap, resources?: StringStringMap, helpers?: StringStringMap, logs?: StringStringMap }; } @@ -104,6 +105,9 @@ export const WorkunitDetails: React.FunctionComponent = ({ }, { id: "eclsummary", label: nlsHPCC.ECL + }, { + id: "eclsummaryold", + label: "L" + nlsHPCC.ECL }, { id: "xml", label: nlsHPCC.XML @@ -155,7 +159,7 @@ export const WorkunitDetails: React.FunctionComponent = ({ - + diff --git a/esp/src/src-react/hooks/metrics.ts b/esp/src/src-react/hooks/metrics.ts index c01b1c3adc3..1611b21896e 100644 --- a/esp/src/src-react/hooks/metrics.ts +++ b/esp/src/src-react/hooks/metrics.ts @@ -1,6 +1,6 @@ import * as React from "react"; import { useConst, useForceUpdate } from "@fluentui/react-hooks"; -import { WUDetailsMeta, WorkunitsService } from "@hpcc-js/comms"; +import { WUDetails, WUDetailsMeta, WorkunitsService, IScope } from "@hpcc-js/comms"; import { userKeyValStore } from "src/KeyValStore"; import { useWorkunit } from "./workunit"; import { useCounter } from "./util"; @@ -101,10 +101,24 @@ export enum FetchStatus { COMPLETE } -export function useWorkunitMetrics(wuid: string): [any[], { [id: string]: any }, WUDetailsMeta.Activity[], WUDetailsMeta.Property[], string[], string[], FetchStatus, () => void] { +const scopeFilterDefault: WUDetails.RequestNS.ScopeFilter = { + MaxDepth: 999999, + ScopeTypes: [] +}; + +const nestedFilterDefault: WUDetails.RequestNS.NestedFilter = { + Depth: 0, + ScopeTypes: [] +}; + +export function useWorkunitMetrics( + wuid: string, + scopeFilter: WUDetails.RequestNS.ScopeFilter = scopeFilterDefault, + nestedFilter: WUDetails.RequestNS.NestedFilter = nestedFilterDefault +): [IScope[], { [id: string]: any }, WUDetailsMeta.Activity[], WUDetailsMeta.Property[], string[], string[], FetchStatus, () => void] { const [workunit, state] = useWorkunit(wuid); - const [data, setData] = React.useState([]); + const [data, setData] = React.useState([]); const [columns, setColumns] = React.useState<{ [id: string]: any }>([]); const [activities, setActivities] = React.useState([]); const [properties, setProperties] = React.useState([]); @@ -116,14 +130,8 @@ export function useWorkunitMetrics(wuid: string): [any[], { [id: string]: any }, React.useEffect(() => { setStatus(FetchStatus.STARTED); workunit?.fetchDetailsNormalized({ - ScopeFilter: { - MaxDepth: 999999, - ScopeTypes: [] - }, - NestedFilter: { - Depth: 0, - ScopeTypes: [] - }, + ScopeFilter: scopeFilter, + NestedFilter: nestedFilter, PropertiesToReturn: { AllScopes: true, AllAttributes: true, @@ -158,7 +166,7 @@ export function useWorkunitMetrics(wuid: string): [any[], { [id: string]: any }, }).finally(() => { setStatus(FetchStatus.COMPLETE); }); - }, [workunit, state, count]); + }, [workunit, state, count, scopeFilter, nestedFilter]); return [data, columns, activities, properties, measures, scopeTypes, status, increment]; } diff --git a/esp/src/src-react/hooks/workunit.ts b/esp/src/src-react/hooks/workunit.ts index 228656012b4..9faff42ea3f 100644 --- a/esp/src/src-react/hooks/workunit.ts +++ b/esp/src/src-react/hooks/workunit.ts @@ -6,6 +6,7 @@ import nlsHPCC from "src/nlsHPCC"; import * as Utility from "src/Utility"; import { singletonDebounce } from "../util/throttle"; import { useCounter } from "./util"; +import { Archive } from "../util/metricArchive"; const logger = scopedLogger("../hooks/workunit.ts"); type RefreshFunc = (full?: boolean) => Promise; @@ -236,6 +237,45 @@ export function useWorkunitResources(wuid: string): [string[], Workunit, WUState return [resources, workunit, state, increment]; } +export function useWorkunitQuery(wuid: string): [string, Workunit, WUStateID, () => void] { + + const [workunit, state] = useWorkunit(wuid); + const [query, setQuery] = React.useState(""); + const [count, increment] = useCounter(); + + React.useEffect(() => { + if (workunit) { + const fetchQuery = singletonDebounce(workunit, "fetchQuery"); + fetchQuery().then(response => { + setQuery(response?.Text ?? ""); + }).catch(err => logger.error(err)); + } + }, [workunit, state, count]); + + return [query, workunit, state, increment]; +} + +export function useWorkunitArchive(wuid: string): [string, Workunit, WUStateID, Archive, () => void] { + + const [workunit, state] = useWorkunit(wuid); + const [archiveString, setArchiveString] = React.useState(""); + const [archive, setArchive] = React.useState(); + const [count, increment] = useCounter(); + + React.useEffect(() => { + if (workunit) { + const fetchArchive = singletonDebounce(workunit, "fetchArchive"); + fetchArchive().then(response => { + setArchiveString(response); + const archive = new Archive(response); + setArchive(archive); + }).catch(err => logger.error(err)); + } + }, [workunit, state, count]); + + return [archiveString, workunit, state, archive, increment]; +} + export interface HelperRow { id: string; Type: string; diff --git a/esp/src/src-react/util/metricArchive.ts b/esp/src/src-react/util/metricArchive.ts new file mode 100644 index 00000000000..a5d85780985 --- /dev/null +++ b/esp/src/src-react/util/metricArchive.ts @@ -0,0 +1,197 @@ +import { IScope } from "@hpcc-js/comms"; +import { XMLNode, xml2json } from "@hpcc-js/util"; +import nlsHPCC from "src/nlsHPCC"; + +export class Archive { + build: string = ""; + eclVersion: string = ""; + legacyImport: string = ""; + legacyWhen: string = ""; + + query: Query; + + modAttrs: Array = []; + modules: Module[] = []; + attributes: Attribute[] = []; + + private attrId_Attribute: { [id: string]: Attribute } = {}; + private sourcePath_Attribute: { [path: string]: Attribute } = {}; + private sourcePath_Metrics: { [id: string]: Set } = {}; + private sourcePath_TimeTotalExecute: { [path: string]: { total: number, line: { [no: number]: { total: number } } } } = {}; + private _metrics: any[] = []; + + private _timeTotalExecute = 0; + get timeTotalExecute() { return this._timeTotalExecute; } + + constructor(xml: string) { + const archiveJson = xml2json(xml); + this.walkArchiveJson(archiveJson); + } + + protected walkArchiveJson(node: XMLNode, parentModule?: Module, parentQualifiedId?: string, depth: number = 0) { + let qualifiedId = ""; + if (parentQualifiedId && node.$?.key) { + qualifiedId = `${parentQualifiedId}.${node.$?.key}`; + } else if (node.$?.key) { + qualifiedId = node.$?.key; + } + + let module: Module; + switch (node.name) { + case "Archive": + this.build = node.$.build; + this.eclVersion = node.$.eclVersion; + this.legacyImport = node.$.legacyImport; + this.legacyWhen = node.$.legacyWhen; + break; + case "Query": + this.query = new Query(node.$.attributePath, node.content.trim()); + break; + case "Module": + if (qualifiedId && node.children().length) { + module = new Module(parentQualifiedId, qualifiedId, node.$.name, depth); + this.modAttrs.push(module); + this.modules.push(module); + if (parentModule) { + parentModule.modules.push(module); + } + } + break; + case "Attribute": + const attribute = new Attribute(parentQualifiedId, qualifiedId, node.$.name, node.$.sourcePath, node.$.ts, node.content.trim(), depth); + this.modAttrs.push(attribute); + this.attributes.push(attribute); + this.sourcePath_Attribute[attribute.sourcePath] = attribute; + this.attrId_Attribute[attribute.id] = attribute; + if (parentModule) { + parentModule.attributes.push(attribute); + } + break; + default: + } + node.children().forEach(child => { + this.walkArchiveJson(child, module, qualifiedId, depth + 1); + }); + switch (node.name) { + case "Archive": + if (this.modAttrs.length === 0) { + const attribute = new Attribute("", "undefined", `<${nlsHPCC.undefined}>`, "", "", this.query.content, depth); + this.query = new Query("undefined", ""); + this.modAttrs.push(attribute); + this.attributes.push(attribute); + this.attrId_Attribute[attribute.id] = attribute; + } + break; + default: + } + } + + updateMetrics(metrics: any[]) { + this._metrics = metrics; + this.sourcePath_TimeTotalExecute = {}; + this._timeTotalExecute = metrics.filter(metric => !!metric.DefinitionList).reduce((prev, metric) => { + const totalTime = metric.TimeMaxTotalExecute ?? metric.TimeAvgTotalExecute ?? metric.TimeTotalExecute ?? 0; + metric.DefinitionList?.forEach((definition, idx) => { + if (!this.sourcePath_TimeTotalExecute[definition.filePath]) { + this.sourcePath_TimeTotalExecute[definition.filePath] = { total: 0, line: {} }; + } + // if (idx === 0) { + this.sourcePath_TimeTotalExecute[definition.filePath].total += totalTime; + // } + const line = definition.line; + if (!this.sourcePath_TimeTotalExecute[definition.filePath].line[line]) { + this.sourcePath_TimeTotalExecute[definition.filePath].line[line] = { total: 0 }; + } + this.sourcePath_TimeTotalExecute[definition.filePath].line[line].total += totalTime ?? 0; + + if (!this.sourcePath_Metrics[definition.filePath]) { + this.sourcePath_Metrics[definition.filePath] = new Set(); + } + this.sourcePath_Metrics[definition.filePath].add(metric.id); + }); + return prev + totalTime ?? 0; + }, 0); + } + + sourcePathTime(sourcePath: string) { + return this.sourcePath_TimeTotalExecute[sourcePath]?.total ?? 0; + } + + attribute(attrId: string) { + return this.attrId_Attribute[attrId]; + } + + sourcePath(attrId: string) { + const attr = this.attribute(attrId); + return attr?.sourcePath ?? ""; + } + + content(attrId: string) { + const attr = this.attribute(attrId); + return attr?.content ?? ""; + } + + markers(id: string) { + const retVal = []; + const attr = this.attribute(id); + if (!attr) return retVal; + const fileTime = this.sourcePath_TimeTotalExecute[attr.sourcePath]; + if (!fileTime) return retVal; + for (const lineNum in fileTime.line) { + const label = Math.round((fileTime.line[lineNum].total / fileTime.total) * 100); + retVal.push({ + lineNum, + label + }); + } + return retVal; + } + + queryId(): string { + return this.query?.attributePath?.toLowerCase() ?? ""; + } + + metricIDs(attrId: string): string[] { + const attr = this.attribute(attrId); + if (!attr) return []; + const set = this.sourcePath_Metrics[attr.sourcePath]; + if (!set) return []; + return [...set.values()]; + } + + metrics(attrId: string): IScope[] { + const metricIDs = this.metricIDs(attrId); + return this._metrics.filter(metric => metricIDs.includes(metric.id)); + } +} + +class Query { + constructor(readonly attributePath: string, readonly content: string) { + } +} + +export class Module { + readonly type: string = "Module"; + modules: Module[] = []; + attributes: Attribute[] = []; + + constructor(readonly parentId: string, readonly id: string, readonly name: string, readonly depth: number) { + } +} + +export class Attribute { + readonly type: string = "Attribute"; + constructor(readonly parentId: string, readonly id: string, readonly name: string, readonly sourcePath: string, readonly ts: string, readonly content: string, readonly depth: number) { + } +} +export function isAttribute(obj: any): obj is Attribute { + return obj.type === "Attribute"; +} + +export interface FileMetric { + path: string; + row: number; + col: number; + metric: object; +} +