mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-10-31 07:55:06 +01:00 
			
		
		
		
	Merge branch 'hotfix' into add_detailed_nodelist
This commit is contained in:
		| @@ -76,7 +76,7 @@ | ||||
|     numHWThreads: filterPresets.numHWThreads || { from: null, to: null }, | ||||
|     numAccelerators: filterPresets.numAccelerators || { from: null, to: null }, | ||||
|  | ||||
|     stats: [], | ||||
|     stats: filterPresets.stats || [], | ||||
|   }; | ||||
|  | ||||
|   let isClusterOpen = false, | ||||
| @@ -127,27 +127,30 @@ | ||||
|       items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } }); | ||||
|     if (filters.arrayJobId != null) | ||||
|       items.push({ arrayJobId: filters.arrayJobId }); | ||||
|     if (filters.numNodes.from != null || filters.numNodes.to != null) | ||||
|     if (filters.numNodes.from != null || filters.numNodes.to != null) { | ||||
|       items.push({ | ||||
|         numNodes: { from: filters.numNodes.from, to: filters.numNodes.to }, | ||||
|       }); | ||||
|     if (filters.numHWThreads.from != null || filters.numHWThreads.to != null) | ||||
|       isNodesModified = true; | ||||
|     } | ||||
|     if (filters.numHWThreads.from != null || filters.numHWThreads.to != null) { | ||||
|       items.push({ | ||||
|         numHWThreads: { | ||||
|           from: filters.numHWThreads.from, | ||||
|           to: filters.numHWThreads.to, | ||||
|         }, | ||||
|       }); | ||||
|     if ( | ||||
|       filters.numAccelerators.from != null || | ||||
|       filters.numAccelerators.to != null | ||||
|     ) | ||||
|       isHwthreadsModified = true; | ||||
|     } | ||||
|     if (filters.numAccelerators.from != null || filters.numAccelerators.to != null) { | ||||
|       items.push({ | ||||
|         numAccelerators: { | ||||
|           from: filters.numAccelerators.from, | ||||
|           to: filters.numAccelerators.to, | ||||
|         }, | ||||
|       }); | ||||
|       isAccsModified = true; | ||||
|     } | ||||
|     if (filters.user) | ||||
|       items.push({ user: { [filters.userMatch]: filters.user } }); | ||||
|     if (filters.project) | ||||
| @@ -197,10 +200,10 @@ | ||||
|       opts.push(`energy=${filters.energy.from}-${filters.energy.to}`); | ||||
|     if (filters.numNodes.from && filters.numNodes.to) | ||||
|       opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`); | ||||
|     if (filters.numHWThreads.from && filters.numHWThreads.to) | ||||
|       opts.push(`numHWThreads=${filters.numHWThreads.from}-${filters.numHWThreads.to}`); | ||||
|     if (filters.numAccelerators.from && filters.numAccelerators.to) | ||||
|       opts.push( | ||||
|         `numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`, | ||||
|       ); | ||||
|       opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`); | ||||
|     if (filters.user.length != 0) | ||||
|       if (filters.userMatch != "in") { | ||||
|         opts.push(`user=${filters.user}`); | ||||
| @@ -214,7 +217,10 @@ | ||||
|     if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`); | ||||
|     if (filters.project && filters.projectMatch != "contains") | ||||
|       opts.push(`projectMatch=${filters.projectMatch}`); | ||||
|  | ||||
|     if (filters.stats.length != 0) | ||||
|       for (let stat of filters.stats) { | ||||
|           opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`); | ||||
|       } | ||||
|     if (opts.length == 0 && window.location.search.length <= 1) return; | ||||
|  | ||||
|     let newurl = `${window.location.pathname}?${opts.join("&")}`; | ||||
| @@ -364,8 +370,7 @@ | ||||
|     {#if (isNodesModified || isHwthreadsModified) && isAccsModified}, | ||||
|     {/if} | ||||
|     {#if isAccsModified} | ||||
|       Accelerators: {filters.numAccelerators.from} - {filters | ||||
|         .numAccelerators.to} | ||||
|       Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to} | ||||
|     {/if} | ||||
|   </Info> | ||||
| {/if} | ||||
| @@ -385,7 +390,7 @@ | ||||
| {#if filters.stats.length > 0} | ||||
|   <Info icon="bar-chart" on:click={() => (isStatsOpen = true)}> | ||||
|     {filters.stats | ||||
|       .map((stat) => `${stat.text}: ${stat.from} - ${stat.to}`) | ||||
|       .map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`) | ||||
|       .join(", ")} | ||||
|   </Info> | ||||
| {/if} | ||||
|   | ||||
| @@ -30,6 +30,10 @@ | ||||
|     initialized = getContext("initialized"), | ||||
|     globalMetrics = getContext("globalMetrics"); | ||||
|  | ||||
|   const equalsCheck = (a, b) => { | ||||
|     return JSON.stringify(a) === JSON.stringify(b); | ||||
|   } | ||||
|  | ||||
|   export let sorting = { field: "startTime", type: "col", order: "DESC" }; | ||||
|   export let matchedJobs = 0; | ||||
|   export let metrics = ccconfig.plot_list_selectedMetrics; | ||||
| @@ -40,6 +44,8 @@ | ||||
|   let page = 1; | ||||
|   let paging = { itemsPerPage, page }; | ||||
|   let filter = []; | ||||
|   let lastFilter = []; | ||||
|   let lastSorting = null; | ||||
|   let triggerMetricRefresh = false; | ||||
|  | ||||
|   function getUnit(m) { | ||||
| @@ -105,9 +111,34 @@ | ||||
|     variables: { paging, sorting, filter }, | ||||
|   }); | ||||
|  | ||||
|   let jobs = [] | ||||
|   $: if (!usePaging && sorting) { | ||||
|     // console.log('Reset Paging ...') | ||||
|     paging = { itemsPerPage: 10, page: 1 } | ||||
|   }; | ||||
|  | ||||
|   let jobs = []; | ||||
|   $: if ($initialized && $jobsStore.data) { | ||||
|     jobs = [...$jobsStore.data.jobs.items] | ||||
|     if (usePaging) { | ||||
|       jobs = [...$jobsStore.data.jobs.items] | ||||
|     } else { // Prevents jump to table head in continiuous mode, only if no change in sort or filter | ||||
|       if (equalsCheck(filter, lastFilter) && equalsCheck(sorting, lastSorting)) { | ||||
|         // console.log('Both Equal: Continuous Addition ... Set None') | ||||
|         jobs = jobs.concat([...$jobsStore.data.jobs.items]) | ||||
|       } else if (equalsCheck(filter, lastFilter)) { | ||||
|         // console.log('Filter Equal: Continuous Reset ... Set lastSorting') | ||||
|         lastSorting = { ...sorting } | ||||
|         jobs = [...$jobsStore.data.jobs.items] | ||||
|       } else if (equalsCheck(sorting, lastSorting)) { | ||||
|         // console.log('Sorting Equal: Continuous Reset ... Set lastFilter') | ||||
|         lastFilter = [ ...filter ] | ||||
|         jobs = [...$jobsStore.data.jobs.items] | ||||
|       } else { | ||||
|         // console.log('None Equal: Continuous Reset ... Set lastBoth') | ||||
|         lastSorting = { ...sorting } | ||||
|         lastFilter = [ ...filter ] | ||||
|         jobs = [...$jobsStore.data.jobs.items] | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   $: matchedJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1; | ||||
| @@ -170,7 +201,6 @@ | ||||
|   } | ||||
|  | ||||
|   if (!usePaging) { | ||||
|     let scrollMultiplier = 1 | ||||
|     window.addEventListener('scroll', () => { | ||||
|       let { | ||||
|         scrollTop, | ||||
| @@ -181,8 +211,7 @@ | ||||
|       // Add 100 px offset to trigger load earlier | ||||
|       if (scrollTop + clientHeight >= scrollHeight - 100 && $jobsStore.data != null && $jobsStore.data.jobs.hasNextPage) { | ||||
|         let pendingPaging = { ...paging } | ||||
|         scrollMultiplier += 1 | ||||
|         pendingPaging.itemsPerPage = itemsPerPage * scrollMultiplier | ||||
|         pendingPaging.page += 1 | ||||
|         paging = pendingPaging | ||||
|       }; | ||||
|     }); | ||||
|   | ||||
| @@ -77,6 +77,13 @@ | ||||
|         dispatch("set-filter", { states }); | ||||
|       }}>Close & Apply</Button | ||||
|     > | ||||
|     <Button | ||||
|       color="warning" | ||||
|       on:click={() => { | ||||
|         states = [...allJobStates]; | ||||
|         pendingStates = []; | ||||
|       }}>Deselect All</Button | ||||
|     > | ||||
|     <Button | ||||
|       color="danger" | ||||
|       on:click={() => { | ||||
|   | ||||
| @@ -5,10 +5,10 @@ | ||||
|     - `cluster Object?`: The currently selected cluster config [Default: null] | ||||
|     - `isOpen Bool?`: Is this filter component opened [Default: false] | ||||
|     - `numNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}] | ||||
|     - `numHWThreads Object?`: The currently selected numHWTreads filter [Default: {from:null, to:null}] | ||||
|     - `numHWThreads Object?`: The currently selected numHWThreads filter [Default: {from:null, to:null}] | ||||
|     - `numAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}] | ||||
|     - `isNodesModified Bool?`: Is the node filter modified [Default: false] | ||||
|     - `isHwtreadsModified Bool?`: Is the Hwthreads filter modified [Default: false] | ||||
|     - `isHwthreadsModified Bool?`: Is the Hwthreads filter modified [Default: false] | ||||
|     - `isAccsModified Bool?`: Is the Accelerator filter modified [Default: false] | ||||
|     - `namedNode String?`: The currently selected single named node (= hostname) [Default: null] | ||||
|  | ||||
| @@ -60,7 +60,7 @@ | ||||
|     ); | ||||
|  | ||||
|   // Limited to Single-Node Thread Count | ||||
|   const findMaxNumHWTreadsPerNode = (clusters) => | ||||
|   const findMaxNumHWThreadsPerNode = (clusters) => | ||||
|     clusters.reduce( | ||||
|       (max, cluster) => | ||||
|         Math.max( | ||||
| @@ -91,13 +91,13 @@ | ||||
|         minNumNodes = filterRanges.numNodes.from; | ||||
|         maxNumNodes = filterRanges.numNodes.to; | ||||
|         maxNumAccelerators = findMaxNumAccels([{ subClusters }]); | ||||
|         maxNumHWThreads = findMaxNumHWTreadsPerNode([{ subClusters }]); | ||||
|         maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]); | ||||
|       } else if (clusters.length > 0) { | ||||
|         const { filterRanges } = header.clusters[0]; | ||||
|         minNumNodes = filterRanges.numNodes.from; | ||||
|         maxNumNodes = filterRanges.numNodes.to; | ||||
|         maxNumAccelerators = findMaxNumAccels(clusters); | ||||
|         maxNumHWThreads = findMaxNumHWTreadsPerNode(clusters); | ||||
|         maxNumHWThreads = findMaxNumHWThreadsPerNode(clusters); | ||||
|         for (let cluster of header.clusters) { | ||||
|           const { filterRanges } = cluster; | ||||
|           minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from); | ||||
|   | ||||
| @@ -29,10 +29,11 @@ | ||||
|   export let isOpen = false; | ||||
|   export let stats = []; | ||||
|  | ||||
|   let statistics = [] | ||||
|   let statistics = []; | ||||
|  | ||||
|   function loadRanges(isInitialized) { | ||||
|     if (!isInitialized) return; | ||||
|     statistics = getStatsItems(); | ||||
|     statistics = getStatsItems(stats); | ||||
|   } | ||||
|  | ||||
|   function resetRanges() { | ||||
|   | ||||
| @@ -8,44 +8,6 @@ | ||||
|     - `height String?`: Height of the card [Default: '310px'] | ||||
|  --> | ||||
|  | ||||
| <script context="module"> | ||||
|   function findJobThresholds(job, metricConfig) { | ||||
|     if (!job || !metricConfig) { | ||||
|       console.warn("Argument missing for findJobThresholds!"); | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     // metricConfig is on subCluster-Level | ||||
|     const defaultThresholds = { | ||||
|       peak: metricConfig.peak, | ||||
|       normal: metricConfig.normal, | ||||
|       caution: metricConfig.caution, | ||||
|       alert: metricConfig.alert | ||||
|     }; | ||||
|  | ||||
|     // Job_Exclusivity does not matter, only aggregation | ||||
|     if (metricConfig.aggregation === "avg") { | ||||
|       return defaultThresholds; | ||||
|     } else if (metricConfig.aggregation === "sum") { | ||||
|       const topol = getContext("getHardwareTopology")(job.cluster, job.subCluster) | ||||
|       const jobFraction = job.numHWThreads / topol.node.length; | ||||
|  | ||||
|       return { | ||||
|         peak: round(defaultThresholds.peak * jobFraction, 0), | ||||
|         normal: round(defaultThresholds.normal * jobFraction, 0), | ||||
|         caution: round(defaultThresholds.caution * jobFraction, 0), | ||||
|         alert: round(defaultThresholds.alert * jobFraction, 0), | ||||
|       }; | ||||
|     } else { | ||||
|       console.warn( | ||||
|         "Missing or unkown aggregation mode (sum/avg) for metric:", | ||||
|         metricConfig, | ||||
|       ); | ||||
|       return defaultThresholds; | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <script> | ||||
|   import { getContext } from "svelte"; | ||||
|   import { | ||||
| @@ -59,7 +21,7 @@ | ||||
|     Row, | ||||
|     Col | ||||
|   } from "@sveltestrap/sveltestrap"; | ||||
|   import { round } from "mathjs"; | ||||
|   import { findJobFootprintThresholds } from "../utils.js"; | ||||
|  | ||||
|   export let job; | ||||
|   export let displayTitle = true; | ||||
| @@ -73,8 +35,7 @@ | ||||
|       const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "") | ||||
|  | ||||
|       // Threshold / -Differences | ||||
|       const fmt = findJobThresholds(job, fmc); | ||||
|       if (jf.name === "flops_any") fmt.peak = round(fmt.peak * 0.85, 0); | ||||
|       const fmt = findJobFootprintThresholds(job, jf.stat, fmc); | ||||
|  | ||||
|       // Define basic data -> Value: Use as Provided | ||||
|       const fmBase = { | ||||
| @@ -89,21 +50,21 @@ | ||||
|         return { | ||||
|           ...fmBase, | ||||
|           color: "danger", | ||||
|           message: `Metric average way ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`, | ||||
|           message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`, | ||||
|           impact: 3 | ||||
|         }; | ||||
|       } else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) { | ||||
|         return { | ||||
|           ...fmBase, | ||||
|           color: "warning", | ||||
|           message: `Metric average ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`, | ||||
|           message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`, | ||||
|           impact: 2, | ||||
|         }; | ||||
|       } else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) { | ||||
|         return { | ||||
|           ...fmBase, | ||||
|           color: "success", | ||||
|           message: "Metric average within expected thresholds.", | ||||
|           message: "Footprint value within expected thresholds.", | ||||
|           impact: 1, | ||||
|         }; | ||||
|       } else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) { | ||||
| @@ -111,7 +72,7 @@ | ||||
|           ...fmBase, | ||||
|           color: "info", | ||||
|           message: | ||||
|             "Metric average above expected normal thresholds: Check for artifacts recommended.", | ||||
|             "Footprint value above expected normal threshold: Check for artifacts recommended.", | ||||
|           impact: 0, | ||||
|         }; | ||||
|       } else { | ||||
| @@ -119,7 +80,7 @@ | ||||
|           ...fmBase, | ||||
|           color: "secondary", | ||||
|           message: | ||||
|             "Metric average above expected peak threshold: Check for artifacts!", | ||||
|             "Footprint value above expected peak threshold: Check for artifacts!", | ||||
|           impact: -1, | ||||
|         }; | ||||
|       } | ||||
| @@ -136,25 +97,25 @@ | ||||
|     return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0)); | ||||
|   });; | ||||
|  | ||||
|   function evalFootprint(mean, thresholds, lowerIsBetter, level) { | ||||
|   function evalFootprint(value, thresholds, lowerIsBetter, level) { | ||||
|     // Handle Metrics in which less value is better | ||||
|     switch (level) { | ||||
|       case "peak": | ||||
|         if (lowerIsBetter) | ||||
|           return false; // metric over peak -> return false to trigger impact -1 | ||||
|         else return mean <= thresholds.peak && mean > thresholds.normal; | ||||
|         else return value <= thresholds.peak && value > thresholds.normal; | ||||
|       case "alert": | ||||
|         if (lowerIsBetter) | ||||
|           return mean <= thresholds.peak && mean >= thresholds.alert; | ||||
|         else return mean <= thresholds.alert && mean >= 0; | ||||
|           return value <= thresholds.peak && value >= thresholds.alert; | ||||
|         else return value <= thresholds.alert && value >= 0; | ||||
|       case "caution": | ||||
|         if (lowerIsBetter) | ||||
|           return mean < thresholds.alert && mean >= thresholds.caution; | ||||
|         else return mean <= thresholds.caution && mean > thresholds.alert; | ||||
|           return value < thresholds.alert && value >= thresholds.caution; | ||||
|         else return value <= thresholds.caution && value > thresholds.alert; | ||||
|       case "normal": | ||||
|         if (lowerIsBetter) | ||||
|           return mean < thresholds.caution && mean >= 0; | ||||
|         else return mean <= thresholds.normal && mean > thresholds.caution; | ||||
|           return value < thresholds.caution && value >= 0; | ||||
|         else return value <= thresholds.normal && value > thresholds.caution; | ||||
|       default: | ||||
|         return false; | ||||
|     } | ||||
| @@ -181,10 +142,14 @@ | ||||
|           > | ||||
|             <div class="mx-1"> | ||||
|               <!-- Alerts Only --> | ||||
|               {#if fpd.impact === 3 || fpd.impact === -1} | ||||
|                 <Icon name="exclamation-triangle-fill" class="text-danger" /> | ||||
|               {#if fpd.impact === 3} | ||||
|               <Icon name="exclamation-triangle-fill" class="text-danger" /> | ||||
|               {:else if fpd.impact === 2} | ||||
|                 <Icon name="exclamation-triangle" class="text-warning" /> | ||||
|               {:else if fpd.impact === 0} | ||||
|                 <Icon name="info-circle" class="text-info" /> | ||||
|               {:else if fpd.impact === -1} | ||||
|                 <Icon name="info-circle-fill" class="text-danger" /> | ||||
|               {/if} | ||||
|               <!-- Emoji for all states--> | ||||
|               {#if fpd.impact === 3} | ||||
| @@ -194,7 +159,7 @@ | ||||
|               {:else if fpd.impact === 1} | ||||
|                 <Icon name="emoji-smile" class="text-success" /> | ||||
|               {:else if fpd.impact === 0} | ||||
|                 <Icon name="emoji-laughing" class="text-info" /> | ||||
|                 <Icon name="emoji-smile" class="text-info" /> | ||||
|               {:else if fpd.impact === -1} | ||||
|                 <Icon name="emoji-dizzy" class="text-danger" /> | ||||
|               {/if} | ||||
|   | ||||
| @@ -120,10 +120,13 @@ | ||||
|  | ||||
|   function matchJobTags(tags, availableTags, type, isAdmin, isSupport) { | ||||
|     const jobTagIds = tags.map((t) => t.id) | ||||
|     if (isAdmin || type == 'used') { // Always show used tags, admin also show all unused | ||||
|  | ||||
|     if (type == 'used') { // Always show used tags | ||||
|       return availableTags.filter((at) => jobTagIds.includes(at.id)) | ||||
|     } else { // ... for unused | ||||
|       if (isSupport) { // ... show global tags for support | ||||
|       if (isAdmin) { // ... show all tags for admin | ||||
|         return availableTags.filter((at) => !jobTagIds.includes(at.id)) | ||||
|       } else if (isSupport) { // ... show global tags for support | ||||
|         return availableTags.filter((at) => !jobTagIds.includes(at.id) && at.scope !== "admin") | ||||
|       } else { // ... show only private tags for user, manager | ||||
|         return availableTags.filter((at) => !jobTagIds.includes(at.id) && at.scope !== "admin" && at.scope !== "global") | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
|  --> | ||||
|  | ||||
| <script> | ||||
|   import { Badge, Button, Icon } from "@sveltestrap/sveltestrap"; | ||||
|   import { Badge, Button, Icon, Tooltip } from "@sveltestrap/sveltestrap"; | ||||
|   import { scrambleNames, scramble } from "../utils.js"; | ||||
|   import Tag from "../helper/Tag.svelte"; | ||||
|   import TagManagement from "../helper/TagManagement.svelte"; | ||||
| @@ -42,12 +42,30 @@ | ||||
|   let displayCheck = false; | ||||
|   function clipJobId(jid) { | ||||
|     displayCheck = true; | ||||
|     navigator.clipboard | ||||
|       .writeText(jid) | ||||
|       .catch((reason) => console.error(reason)); | ||||
|       setTimeout(function () { | ||||
|         displayCheck = false; | ||||
|       }, 1500); | ||||
|     // Navigator clipboard api needs a secure context (https) | ||||
|     if (navigator.clipboard && window.isSecureContext) { | ||||
|         navigator.clipboard | ||||
|           .writeText(jid) | ||||
|           .catch((reason) => console.error(reason)); | ||||
|     } else { | ||||
|       // Workaround: Create, Fill, And Copy Content of Textarea | ||||
|       const textArea = document.createElement("textarea"); | ||||
|       textArea.value = jid; | ||||
|       textArea.style.position = "absolute"; | ||||
|       textArea.style.left = "-999999px"; | ||||
|       document.body.prepend(textArea); | ||||
|       textArea.select(); | ||||
|       try { | ||||
|           document.execCommand('copy'); | ||||
|       } catch (error) { | ||||
|           console.error(error); | ||||
|       } finally { | ||||
|           textArea.remove(); | ||||
|       } | ||||
|     } | ||||
|     setTimeout(function () { | ||||
|       displayCheck = false; | ||||
|     }, 1000); | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| @@ -58,13 +76,18 @@ | ||||
|         <a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a> | ||||
|         ({job.cluster})  | ||||
|       </span> | ||||
|       <Button outline color="secondary" size="sm" title="Copy JobID to Clipboard" on:click={clipJobId(job.jobId)} > | ||||
|       <Button id={`${job.cluster}-${job.jobId}-clipboard`} outline color="secondary" size="sm" on:click={clipJobId(job.jobId)} > | ||||
|         {#if displayCheck} | ||||
|           <Icon name="clipboard2-check-fill"/> Copied | ||||
|           <Icon name="clipboard2-check-fill"/> | ||||
|         {:else} | ||||
|           <Icon name="clipboard2"/> Job ID | ||||
|           <Icon name="clipboard2"/> | ||||
|         {/if} | ||||
|       </Button> | ||||
|       <Tooltip | ||||
|         target={`${job.cluster}-${job.jobId}-clipboard`} | ||||
|         placement="right"> | ||||
|           { displayCheck ? 'Copied!' : 'Copy Job ID to Clipboard' } | ||||
|       </Tooltip> | ||||
|     </span> | ||||
|     {#if job.metaData?.jobName} | ||||
|       {#if job.metaData?.jobName.length <= 25} | ||||
|   | ||||
| @@ -37,6 +37,7 @@ | ||||
|     : ["node"]; | ||||
|   let selectedResolution = resampleDefault; | ||||
|   let zoomStates = {}; | ||||
|   let thresholdStates = {}; | ||||
|  | ||||
|   const cluster = getContext("clusters").find((c) => c.name == job.cluster); | ||||
|   const client = getContextClient(); | ||||
| @@ -80,6 +81,13 @@ | ||||
|         zoomStates[metric] = {...detail.lastZoomState} | ||||
|     } | ||||
|  | ||||
|     if ( // States have to differ, causes deathloop if just set | ||||
|       detail?.lastThreshold && | ||||
|       thresholdStates[metric] !== detail.lastThreshold | ||||
|     ) { // Handle to correctly reset on summed metric scope change | ||||
|       thresholdStates[metric] = detail.lastThreshold; | ||||
|     }  | ||||
|  | ||||
|     if (detail?.newRes) { // Triggers GQL | ||||
|         selectedResolution = detail.newRes | ||||
|     } | ||||
| @@ -191,6 +199,7 @@ | ||||
|             numhwthreads={job.numHWThreads} | ||||
|             numaccs={job.numAcc} | ||||
|             zoomState={zoomStates[metric.data.name] || null} | ||||
|             thresholdState={thresholdStates[metric.data.name] || null} | ||||
|           /> | ||||
|         {:else if metric.disabled == true && metric.data} | ||||
|           <Card body color="info" | ||||
|   | ||||
| @@ -50,7 +50,7 @@ | ||||
|   } | ||||
|  | ||||
|   // removed arg "subcluster": input metricconfig and topology now directly derived from subcluster | ||||
|   function findThresholds( | ||||
|   function findJobAggregationThresholds( | ||||
|     subClusterTopology, | ||||
|     metricConfig, | ||||
|     scope, | ||||
| @@ -60,10 +60,16 @@ | ||||
|   ) { | ||||
|  | ||||
|     if (!subClusterTopology || !metricConfig || !scope) { | ||||
|       console.warn("Argument missing for findThresholds!"); | ||||
|       console.warn("Argument missing for findJobAggregationThresholds!"); | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     // handle special *-stat scopes | ||||
|     if (scope.match(/(.*)-stat$/)) { | ||||
|       const statParts = scope.split('-'); | ||||
|       scope = statParts[0] | ||||
|     } | ||||
|  | ||||
|     if ( | ||||
|       (scope == "node" && isShared == false) || | ||||
|       metricConfig?.aggregation == "avg" | ||||
| @@ -78,19 +84,20 @@ | ||||
|  | ||||
|  | ||||
|     if (metricConfig?.aggregation == "sum") { | ||||
|       let divisor = 1 | ||||
|       let divisor; | ||||
|       if (isShared == true) { // Shared | ||||
|         if (numaccs > 0) divisor = subClusterTopology.accelerators.length / numaccs; | ||||
|         else if (numhwthreads > 0) divisor = subClusterTopology.node.length / numhwthreads; | ||||
|         else if (numhwthreads > 0) divisor = subClusterTopology.core.length / numhwthreads; | ||||
|       } | ||||
|       else if (scope == 'socket') divisor = subClusterTopology.socket.length; | ||||
|       else if (scope == "core") divisor = subClusterTopology.core.length; | ||||
|       else if (scope == "accelerator") | ||||
|         divisor = subClusterTopology.accelerators.length; | ||||
|       else if (scope == "hwthread") divisor = subClusterTopology.node.length; | ||||
|       else if (scope == 'node')         divisor = 1; // Use as configured for nodes | ||||
|       else if (scope == 'socket')       divisor = subClusterTopology.socket.length; | ||||
|       else if (scope == "memoryDomain") divisor = subClusterTopology.memoryDomain.length; | ||||
|       else if (scope == "core")         divisor = subClusterTopology.core.length; | ||||
|       else if (scope == "hwthread")     divisor = subClusterTopology.core.length; // alt. name for core | ||||
|       else if (scope == "accelerator")  divisor = subClusterTopology.accelerators.length; | ||||
|       else { | ||||
|         // console.log('TODO: how to calc thresholds for ', scope) | ||||
|         return null; | ||||
|         console.log('Unknown scope, return default aggregation thresholds ', scope) | ||||
|         divisor = 1; | ||||
|       } | ||||
|  | ||||
|       return { | ||||
| @@ -130,6 +137,7 @@ | ||||
|   export let numhwthreads = 0; | ||||
|   export let numaccs = 0; | ||||
|   export let zoomState = null; | ||||
|   export let thresholdState = null; | ||||
|  | ||||
|   if (useStatsSeries == null) useStatsSeries = statisticsSeries != null; | ||||
|   if (useStatsSeries == false && series == null) useStatsSeries = true; | ||||
| @@ -149,7 +157,7 @@ | ||||
|     caution: "rgba(255, 128, 0, 0.3)", | ||||
|     alert: "rgba(255, 0, 0, 0.3)", | ||||
|   }; | ||||
|   const thresholds = findThresholds( | ||||
|   const thresholds = findJobAggregationThresholds( | ||||
|     subClusterTopology, | ||||
|     metricConfig, | ||||
|     scope, | ||||
| @@ -468,12 +476,14 @@ | ||||
|                 // console.log('Dispatch Zoom with Res from / to', timestep, closest) | ||||
|                 dispatch('zoom', { | ||||
|                   newRes: closest, | ||||
|                   lastZoomState: u?.scales | ||||
|                   lastZoomState: u?.scales, | ||||
|                   lastThreshold: thresholds?.normal | ||||
|                 }); | ||||
|               } | ||||
|             } else { | ||||
|               dispatch('zoom', { | ||||
|                 lastZoomState: u?.scales | ||||
|                 lastZoomState: u?.scales, | ||||
|                 lastThreshold: thresholds?.normal | ||||
|               }); | ||||
|             }; | ||||
|           }; | ||||
| @@ -498,16 +508,19 @@ | ||||
|   let timeoutId = null; | ||||
|  | ||||
|   function render(ren_width, ren_height) { | ||||
|     if (!uplot) { // Init uPlot | ||||
|     if (!uplot) { | ||||
|       opts.width = ren_width; | ||||
|       opts.height = ren_height; | ||||
|       if (zoomState) { | ||||
|       if (zoomState && metricConfig?.aggregation == "avg") { | ||||
|         opts.scales = {...zoomState} | ||||
|       } else if (zoomState && metricConfig?.aggregation == "sum") { | ||||
|         // Allow Zoom In === Ymin changed | ||||
|         if (zoomState.y.min !== 0) { // scope change?: only use zoomState if thresholds match | ||||
|           if ((thresholdState === thresholds?.normal)) { opts.scales = {...zoomState} }; | ||||
|         } // else: reset scaling to default | ||||
|       } | ||||
|       // console.log('Init Sizes ...', { width: opts.width, height: opts.height }) | ||||
|       uplot = new uPlot(opts, plotData, plotWrapper); | ||||
|     } else { // Update size | ||||
|       // console.log('Update uPlot ...', { width: ren_width, height: ren_height }) | ||||
|     } else { | ||||
|       uplot.setSize({ width: ren_width, height: ren_height }); | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -45,7 +45,7 @@ | ||||
|         if (footprintData) { | ||||
|             return footprintData.filter(fpd => { | ||||
|                 if (!jobMetrics.find(m => m.name == fpd.name && m.scope == "node" || fpd.impact == 4)) { | ||||
|                     console.warn(`PolarPlot: No metric data (or config) for '${fpd.name}'`) | ||||
|                     console.warn(`PolarPlot: No metric data for '${fpd.name}'`) | ||||
|                     return false | ||||
|                 } | ||||
|                 return true | ||||
| @@ -72,6 +72,7 @@ | ||||
|     const getMetricConfig = getContext("getMetricConfig"); | ||||
|  | ||||
|     const getValuesForStatGeneric = (getStat) => labels.map(name => { | ||||
|         // TODO: Requires Scaling if Shared Job | ||||
|         const peak = getMetricConfig(cluster, subCluster, name).peak | ||||
|         const metric = jobMetrics.find(m => m.name == name && m.scope == "node") | ||||
|         const value = getStat(metric.metric) / peak | ||||
| @@ -79,6 +80,7 @@ | ||||
|     }) | ||||
|  | ||||
|     const getValuesForStatFootprint = (getStat) => labels.map(name => { | ||||
|         // FootprintData 'Peak' is pre-scaled for Shared Jobs in JobSummary Component | ||||
|         const peak = footprintData.find(fpd => fpd.name === name).peak | ||||
|         const metric = jobMetrics.find(m => m.name == name && m.scope == "node") | ||||
|         const value = getStat(metric.metric) / peak | ||||
| @@ -86,14 +88,21 @@ | ||||
|     }) | ||||
|  | ||||
|     function getMax(metric) { | ||||
|         let max = 0 | ||||
|         let max = metric.series[0].statistics.max; | ||||
|         for (let series of metric.series) | ||||
|             max = Math.max(max, series.statistics.max) | ||||
|         return max | ||||
|     } | ||||
|  | ||||
|     function getMin(metric) { | ||||
|         let min = metric.series[0].statistics.min; | ||||
|         for (let series of metric.series) | ||||
|             min = Math.min(min, series.statistics.min) | ||||
|         return min | ||||
|     } | ||||
|  | ||||
|     function getAvg(metric) { | ||||
|         let avg = 0 | ||||
|         let avg = 0; | ||||
|         for (let series of metric.series) | ||||
|             avg += series.statistics.avg | ||||
|         return avg / metric.series.length | ||||
| @@ -104,6 +113,8 @@ | ||||
|             return getValuesForStatGeneric(getAvg) | ||||
|         } else if (type === 'max') { | ||||
|             return getValuesForStatGeneric(getMax) | ||||
|         } else if (type === 'min') { | ||||
|             return getValuesForStatGeneric(getMin) | ||||
|         } | ||||
|         console.log('Unknown Type For Polar Data') | ||||
|         return [] | ||||
| @@ -114,6 +125,8 @@ | ||||
|             return getValuesForStatFootprint(getAvg) | ||||
|         } else if (type === 'max') { | ||||
|             return getValuesForStatFootprint(getMax) | ||||
|         } else if (type === 'min') { | ||||
|             return getValuesForStatFootprint(getMin) | ||||
|         } | ||||
|         console.log('Unknown Type For Polar Data') | ||||
|         return [] | ||||
| @@ -124,25 +137,36 @@ | ||||
|         datasets: [ | ||||
|             { | ||||
|                 label: 'Max', | ||||
|                 data: footprintData ? loadDataForFootprint('max') : loadDataGeneric('max'), //  | ||||
|                 data: footprintData ? loadDataForFootprint('max') : loadDataGeneric('max'), // Node Scope Only | ||||
|                 fill: 1, | ||||
|                 backgroundColor: 'rgba(0, 102, 255, 0.25)', | ||||
|                 borderColor: 'rgb(0, 102, 255)', | ||||
|                 pointBackgroundColor: 'rgb(0, 102, 255)', | ||||
|                 backgroundColor: 'rgba(0, 0, 255, 0.25)', | ||||
|                 borderColor: 'rgb(0, 0, 255)', | ||||
|                 pointBackgroundColor: 'rgb(0, 0, 255)', | ||||
|                 pointBorderColor: '#fff', | ||||
|                 pointHoverBackgroundColor: '#fff', | ||||
|                 pointHoverBorderColor: 'rgb(0, 102, 255)' | ||||
|                 pointHoverBorderColor: 'rgb(0, 0, 255)' | ||||
|             }, | ||||
|             { | ||||
|                 label: 'Avg', | ||||
|                 data: footprintData ? loadDataForFootprint('avg') : loadDataGeneric('avg'), // getValuesForStat(getAvg) | ||||
|                 fill: true, | ||||
|                 backgroundColor: 'rgba(255, 153, 0, 0.25)', | ||||
|                 borderColor: 'rgb(255, 153, 0)', | ||||
|                 pointBackgroundColor: 'rgb(255, 153, 0)', | ||||
|                 data: footprintData ? loadDataForFootprint('avg') : loadDataGeneric('avg'), // Node Scope Only | ||||
|                 fill: 2, | ||||
|                 backgroundColor: 'rgba(255, 210, 0, 0.25)', | ||||
|                 borderColor: 'rgb(255, 210, 0)', | ||||
|                 pointBackgroundColor: 'rgb(255, 210, 0)', | ||||
|                 pointBorderColor: '#fff', | ||||
|                 pointHoverBackgroundColor: '#fff', | ||||
|                 pointHoverBorderColor: 'rgb(255, 153, 0)' | ||||
|                 pointHoverBorderColor: 'rgb(255, 210, 0)' | ||||
|             }, | ||||
|             { | ||||
|                 label: 'Min', | ||||
|                 data: footprintData ? loadDataForFootprint('min') : loadDataGeneric('min'), // Node Scope Only | ||||
|                 fill: true, | ||||
|                 backgroundColor: 'rgba(255, 0, 0, 0.25)', | ||||
|                 borderColor: 'rgb(255, 0, 0)', | ||||
|                 pointBackgroundColor: 'rgb(255, 0, 0)', | ||||
|                 pointBorderColor: '#fff', | ||||
|                 pointHoverBackgroundColor: '#fff', | ||||
|                 pointHoverBorderColor: 'rgb(255, 0, 0)' | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { | ||||
| } from "@urql/svelte"; | ||||
| import { setContext, getContext, hasContext, onDestroy, tick } from "svelte"; | ||||
| import { readable } from "svelte/store"; | ||||
| import { round } from "mathjs"; | ||||
|  | ||||
| /* | ||||
|  * Call this function only at component initialization time! | ||||
| @@ -318,23 +319,34 @@ export function checkMetricsDisabled(ma, c, s) { // [m]etric[a]rray, [c]luster, | ||||
|     return result | ||||
| } | ||||
|  | ||||
| export function getStatsItems() { | ||||
| export function getStatsItems(presetStats = []) { | ||||
|     // console.time('stats') | ||||
|     const globalMetrics = getContext("globalMetrics") | ||||
|     const result = globalMetrics.map((gm) => { | ||||
|         if (gm?.footprint) { | ||||
|             // console.time('deep') | ||||
|             const mc = getMetricConfigDeep(gm.name, null, null) | ||||
|             // console.timeEnd('deep') | ||||
|             if (mc) { | ||||
|                 return { | ||||
|                     field: gm.name + '_' + gm.footprint, | ||||
|                     text: gm.name + ' (' + gm.footprint + ')', | ||||
|                     metric: gm.name, | ||||
|                     from: 0, | ||||
|                     to: mc.peak, | ||||
|                     peak: mc.peak, | ||||
|                     enabled: false | ||||
|                 const presetEntry = presetStats.find((s) => s?.field === (gm.name + '_' + gm.footprint)) | ||||
|                 if (presetEntry) { | ||||
|                     return { | ||||
|                         field: gm.name + '_' + gm.footprint, | ||||
|                         text: gm.name + ' (' + gm.footprint + ')', | ||||
|                         metric: gm.name, | ||||
|                         from: presetEntry.from, | ||||
|                         to: presetEntry.to, | ||||
|                         peak: mc.peak, | ||||
|                         enabled: true | ||||
|                     } | ||||
|                 } else { | ||||
|                     return { | ||||
|                         field: gm.name + '_' + gm.footprint, | ||||
|                         text: gm.name + ' (' + gm.footprint + ')', | ||||
|                         metric: gm.name, | ||||
|                         from: 0, | ||||
|                         to: mc.peak, | ||||
|                         peak: mc.peak, | ||||
|                         enabled: false | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -344,6 +356,38 @@ export function getStatsItems() { | ||||
|     return [...result]; | ||||
| }; | ||||
|  | ||||
| export function findJobFootprintThresholds(job, stat, metricConfig) { | ||||
|     if (!job || !metricConfig || !stat) { | ||||
|         console.warn("Argument missing for findJobThresholds!"); | ||||
|         return null; | ||||
|     } | ||||
|     // metricConfig is on subCluster-Level | ||||
|     const defaultThresholds = { | ||||
|         peak: metricConfig.peak, | ||||
|         normal: metricConfig.normal, | ||||
|         caution: metricConfig.caution, | ||||
|         alert: metricConfig.alert | ||||
|     }; | ||||
|     /* | ||||
|         Footprints should be comparable: | ||||
|         Always use unchanged single node thresholds for exclusive jobs and "avg" Footprints. | ||||
|         For shared jobs, scale thresholds by the fraction of the job's HWThreads to the node's HWThreads. | ||||
|         'stat' is one of: avg, min, max | ||||
|     */ | ||||
|     if (job.exclusive === 1 || stat === "avg") { | ||||
|         return defaultThresholds | ||||
|     } else { | ||||
|         const topol = getContext("getHardwareTopology")(job.cluster, job.subCluster) | ||||
|         const jobFraction = job.numHWThreads / topol.node.length; | ||||
|         return { | ||||
|         peak: round(defaultThresholds.peak * jobFraction, 0), | ||||
|         normal: round(defaultThresholds.normal * jobFraction, 0), | ||||
|         caution: round(defaultThresholds.caution * jobFraction, 0), | ||||
|         alert: round(defaultThresholds.alert * jobFraction, 0), | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export function getSortItems() { | ||||
|     //console.time('sort') | ||||
|     const globalMetrics = getContext("globalMetrics") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user